diff --git a/.gitignore b/.gitignore index 40be120c..fac11e90 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ target /src/test/test.iml <<<<<<< HEAD /.idea +<<<<<<< HEAD +src/main/resources/account-private-key-google-api-devstarter.json +======= ======= /.idea >>>>>>> 080cd17ae17b68b4cec6512498198d8332690c7c +>>>>>>> 969a9f11bc9654c66161ec7983f43c0638a92a2e diff --git a/README.md b/README.md index a058d17c..72d58a2f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ============================ [презетация](http://youtu.be/__ibkaMRHZI), [ii.ayfaar.org](http://ii.ayfaar.org), [канал YouTube](https://www.youtube.com/channel/UCx7OZ2t2mEiaW6kem5lfl9w) -Ключевые слова: OOP, [SOLID](http://ru.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)), Java, J2SE, Hibernate, Spring (IoC, MVC), JUnit, JavaScript, HTML5, CSS3, [KendoUI](www.kendoui.com), [AngularJS](https://angularjs.org), MySQL, Maven, git, TDD, CI, IntelliJ IDEA +Ключевые слова: OOP, [SOLID](http://ru.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)), Java 8, J2SE, Hibernate, [Spring Boot](https://spring.io/) (JPA, IoC, MVC), JUnit, JavaScript, HTML5, CSS3, [KendoUI](www.kendoui.com), [AngularJS](https://angularjs.org), MySQL, Maven, git, TDD, CI, IntelliJ IDEA Мой скайп: iu3116 @@ -28,27 +28,23 @@ С чего начать (Java) ==================== -**Видео инструкция https://www.youtube.com/watch?v=mbwN4eaES78** - Устанавливаем: 1. GIT http://msysgit.github.io -2. Добавляем git.exe в [переменную окружения Path](http://clip2net.com/s/iuLWXk) и перезагружаем windows +2. Добавляем git.exe в переменную окружения Path и перезагружаем windows 3. Выполняем тестовую задачу [Тренировка работы с git](https://github.com/devstarter/ii/issues/4) 4. [IntelliJ IDEA](http://www.jetbrains.com/idea/download/) -5. [Java version 1.7](https://www.java.com/en/download) -6. [Java SE Development Kit 7](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html) +5. [Java](https://www.java.com/en/download) +6. [Java SE Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/index.html) 7. [MySQL](http://dev.mysql.com/downloads/mysql/) или [XAMPP](https://www.apachefriends.org/index.html) [wiki/База данных](https://github.com/devstarter/ii/wiki/%D0%91%D0%B0%D0%B7%D0%B0-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85) -8. Не обязательно, [Apache Tomcat](http://tomcat.apache.org/download-70.cgi) или [XAMPP](https://www.apachefriends.org/index.html) Окрываем проект: 1. Зарегистрируйтесь в [GitHub](https://github.com) 2. Сделайте Fork (копию) [этого кода](https://github.com/devstarter/ii) из своего акаунта 3. Скачайте его на свой компьютер `git clone https://github.com/<ваш акаунт>/ii.git` -4. Откройте проект с помощью IDEA -5. Устанавите плагин [lombok](http://plugins.jetbrains.com/plugin/6317) для IDEA -6. Запустите тест [RunTest.java](https://github.com/devstarter/ii/blob/master/src/test/java/RunTest.java) +4. Устанавите плагин [lombok](http://plugins.jetbrains.com/plugin/6317) для IDEA +5. [Открываем проект в IntelliJ IDEA](https://github.com/devstarter/ii/wiki/%D0%9E%D1%82%D0%BA%D1%80%D1%8B%D1%82%D0%B8%D0%B5-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0-%D0%B2-IntelliJ-IDEA) Настраиваем базу данных (MySQL) [Видео](https://www.youtube.com/watch?v=l-ZGmR98d-4): @@ -56,11 +52,9 @@ 2. Качаем [последний дамп данных](https://github.com/devstarter/ii/tree/master/db) 3. Импортируем дамп -Запускаем проект (не обязательно): +Запускаем проект: -1. Добавляем Run Configuration для Tomcat в IDEA -2. Запускаем эту конфигурацию +Конфигурация в IDEA [Run/Debug Configuration](https://cloud.githubusercontent.com/assets/1183619/14000544/b469080e-f152-11e5-9a3a-c1acb737b5d8.png) -Подробнее в видео https://www.youtube.com/watch?v=mbwN4eaES78 [![Презетация](http://img.youtube.com/vi/__ibkaMRHZI/0.jpg)](http://youtu.be/__ibkaMRHZI) diff --git a/db/dump-latest.7z b/db/dump-latest.7z new file mode 100644 index 00000000..16b04364 Binary files /dev/null and b/db/dump-latest.7z differ diff --git a/db/dump-latest.sql.tgz b/db/dump-latest.sql.tgz deleted file mode 100644 index 82f802c5..00000000 Binary files a/db/dump-latest.sql.tgz and /dev/null differ diff --git a/pom.xml b/pom.xml index 8169c581..3e43cdef 100755 --- a/pom.xml +++ b/pom.xml @@ -1,398 +1,310 @@ + - - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 org.ayfaar app - 1.0-SNAPSHOT - war + 1.5-SNAPSHOT + jar Ayfaar II App - - 2.0-SNAPSHOT - 1.7.1 - 3.0 - 3.2.5.RELEASE - 3.1.3.RELEASE - 4.2.5.Final - 1.0.6 - 1.6.1 - 2.1.2 - 6.1.24 - 1.6 - UTF-8 - 4.11 - 1.6 - 1.6 - + + org.springframework.boot + spring-boot-starter-parent + 1.3.3.RELEASE + + + + + 8.0.9 + UTF-8 + 1.8 + 1.8 + 1.8 + 1.8 + 4.11 + 3.9 + 0.6.0 + 1.14 + + + + + one.util + streamex + ${streamex.version} + + + org.hamcrest + hamcrest-all + 1.3 + test + + + com.jayway.restassured + rest-assured + 2.9.0 + test + + + org.apache.poi + poi-ooxml + ${poi.version} + test + + + org.apache.poi + poi + ${poi.version} + test + + + org.jsoup + jsoup + 1.7.3 + test + + + net.sf.opencsv + opencsv + 2.0 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + junit + junit + ${junit.version} + test + + + + + commons-collections + commons-collections + 3.2.1 + + + org.apache.commons + commons-lang3 + 3.0 + + + commons-lang + commons-lang + 2.6 + + + commons-io + commons-io + 2.4 + - - - net.sourceforge.htmlcleaner - htmlcleaner - 2.8 - - - com.evernote - evernote-api - 1.25.1 - - - net.sf.opencsv - opencsv - 2.0 - - - net.sourceforge - jwbf - 2.0.0 - - - org.docx4j - docx4j - 2.8.1 - - - joda-time - joda-time - 2.3 - - - net.sf.dozer - dozer - 5.4.0 - - - org.projectlombok - lombok - 0.12.0 - provided - - - com.fasterxml.jackson.core - jackson-core - 2.2.2 - - - com.fasterxml.jackson.core - jackson-annotations - 2.2.2 - - - com.fasterxml.jackson.core - jackson-databind - 2.2.2 - + + com.intellij + annotations + 12.0 + + + commons-beanutils + commons-beanutils + 1.8.3 + + + - - org.mockito - mockito-all - 1.9.5 - test - - - org.mockito - mockito-core - 1.9.5 - test - - - org.reflections - reflections - 0.9.9-RC1 - - - javax.mail - mail - 1.4.6 - - - org.thymeleaf - thymeleaf-spring3 - 2.0.16 - - - org.aspectj - aspectjrt - ${aspects.version} - - - org.aspectj - aspectjweaver - ${aspects.version} - - - junit - junit - ${junit.version} - - - net.coobird - thumbnailator - 0.4.2 - - - commons-fileupload - commons-fileupload - 1.2.2 - - - commons-collections - commons-collections - 3.2.1 - - - org.apache.commons - commons-lang3 - 3.0 - - - commons-io - commons-io - 2.4 - - - com.intellij - annotations - 12.0 - - - commons-beanutils - commons-beanutils - 1.8.3 - - - org.springframework - spring-aspects - ${spring.version} - - - org.springframework - spring-expression - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - - org.codehaus.jackson - jackson-mapper-asl - 1.8.5 - + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.projectlombok + lombok + 1.16.6 + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + test + + + org.springframework.boot + spring-boot-starter-security + + + + mysql + mysql-connector-java + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + javax.inject + javax.inject + 1 + + + com.pushbullet + pushbullet-api + 1.1-SNAPSHOT + + + com.github.dfabulich + sitemapgen4j + 1.0.4 + - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - jul-to-slf4j - ${slf4j.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - ch.qos.logback - logback-access - ${logback.version} - + + + org.apache.httpcomponents + httpclient + 4.5.2 + + + org.apache.httpcomponents + httpmime + 4.5.2 + + + com.google.api-client + google-api-client + 1.22.0 + + + com.google.apis + google-api-services-oauth2 + v2-rev109-1.22.0 + + + com.google.apis + google-api-services-drive + v2-rev135-1.19.0 + + + com.google.apis + google-api-services-sheets + v4-rev38-1.22.0 + + + com.google.apis + google-api-services-youtube + v3-rev162-1.21.0 + + - - org.springframework - spring-web - ${spring.version} - - - org.springframework - spring-test - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - org.springframework - spring-orm - ${spring.version} - - - org.springframework - spring-jdbc - ${spring.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - + + com.google.http-client + google-http-client + 1.21.0 + + + com.google.http-client + google-http-client-jackson2 + 1.21.0 + + + com.google.oauth-client + google-oauth-client-jetty + 1.21.0 + - - - org.springframework.security - spring-security-core - ${spring.security.version} - - - org.springframework.security - spring-security-web - ${spring.security.version} - - - org.springframework.security - spring-security-config - ${spring.security.version} - - - org.springframework.security - spring-security-taglibs - ${spring.security.version} - - - - org.hibernate - hibernate-core - ${hibernate.version} - - - org.hibernate - hibernate-envers - ${hibernate.version} - - - org.hibernate - hibernate-validator - 4.0.2.GA - - - javax.validation - validation-api - 1.0.0.GA - - - mysql - mysql-connector-java - 5.1.13 - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - + + + org.apache.poi + poi-scratchpad + 3.9 + + + net.sf.dozer + dozer + 5.4.0 + - - - javax.persistence - persistence-api - 1.0.2 - - - javassist - javassist - 3.12.1.GA - - - cglib - cglib - 2.2 - + + org.apache.tika + tika-core + ${tika.version} + test + + + org.apache.tika + tika-parsers + ${tika.version} + test + - - org.hsqldb - hsqldb - 2.2.4 - test - - - - - - - - - openshift - - ii - - - maven-war-plugin - 2.1.1 - - webapps - ROOT - - - - - - - - - eap - http://maven.repository.redhat.com/techpreview/all - - true - - - true - - - - russian-morphology.lucene.apache.org - Lucene Russian Morphology Repository for Maven - http://russianmorphology.googlecode.com/svn/repo/releases/ - - - - - eap - http://maven.repository.redhat.com/techpreview/all - - true - - - true - - - - - + + + + ii + + + org.springframework.boot + spring-boot-maven-plugin + org.apache.maven.plugins maven-surefire-plugin @@ -401,12 +313,69 @@ -Dfile.encoding=UTF-8 + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + - - + + + + + eap + http://maven.repository.redhat.com/techpreview/all + + true + + + true + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + pushbullet.java-mvn-repo + https://raw.github.com/devstarter/pushbullet.java/mvn-repo/ + + true + always + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + diff --git a/src/main/java/org/ayfaar/app/Application.java b/src/main/java/org/ayfaar/app/Application.java new file mode 100644 index 00000000..7d590219 --- /dev/null +++ b/src/main/java/org/ayfaar/app/Application.java @@ -0,0 +1,36 @@ +package org.ayfaar.app; + +import lombok.extern.slf4j.Slf4j; +import org.dozer.spring.DozerBeanMapperFactoryBean; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.orm.jpa.EntityScan; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.ImportResource; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableJpaRepositories +@EnableCaching +@EnableAsync +@EnableAspectJAutoProxy +@Slf4j +@EntityScan("org.ayfaar.app.model") +@ComponentScan("org.ayfaar.app") +@ImportResource({"classpath:hibernate.xml", "classpath:spring-basic.xml"}) +public class Application { + public static void main(String[] args) { + final ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); + log.info("Open in browser: " + context.getEnvironment().getProperty("this-url")); + } + + @Bean + public DozerBeanMapperFactoryBean dozerBeanMapperFactoryBean() { + return new DozerBeanMapperFactoryBean(); + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/annotations/ContentsCache.java b/src/main/java/org/ayfaar/app/annotations/ContentsCache.java new file mode 100644 index 00000000..1df185ec --- /dev/null +++ b/src/main/java/org/ayfaar/app/annotations/ContentsCache.java @@ -0,0 +1,15 @@ +package org.ayfaar.app.annotations; + +import org.springframework.cache.annotation.Cacheable; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD}) +@Retention(RUNTIME) +@Cacheable(value = "DBCache", key = "new org.ayfaar.app.controllers.search.cache.ContentsCacheKey(#name)") +public @interface ContentsCache { +} diff --git a/src/main/java/org/ayfaar/app/annotations/Moderated.java b/src/main/java/org/ayfaar/app/annotations/Moderated.java index b43478f6..85247142 100644 --- a/src/main/java/org/ayfaar/app/annotations/Moderated.java +++ b/src/main/java/org/ayfaar/app/annotations/Moderated.java @@ -1,5 +1,7 @@ package org.ayfaar.app.annotations; +import org.ayfaar.app.services.moderation.Action; + import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -9,4 +11,6 @@ @Target({METHOD}) @Retention(RUNTIME) public @interface Moderated { + Action value(); + String command(); } diff --git a/src/main/java/org/ayfaar/app/annotations/SearchResultCache.java b/src/main/java/org/ayfaar/app/annotations/SearchResultCache.java new file mode 100644 index 00000000..95137cc9 --- /dev/null +++ b/src/main/java/org/ayfaar/app/annotations/SearchResultCache.java @@ -0,0 +1,16 @@ +package org.ayfaar.app.annotations; + +import org.springframework.cache.annotation.Cacheable; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Target({METHOD}) +@Retention(RUNTIME) +@Cacheable(value = "DBCache", key = "new org.ayfaar.app.controllers.search.cache.SearchCacheKey(#query, #startFrom, #pageNumber)") +public @interface SearchResultCache { +} diff --git a/src/main/java/org/ayfaar/app/configs/Profile.java b/src/main/java/org/ayfaar/app/configs/Profile.java new file mode 100644 index 00000000..3a23e96c --- /dev/null +++ b/src/main/java/org/ayfaar/app/configs/Profile.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.configs; + +public enum Profile { + prod, dev +} diff --git a/src/main/java/org/ayfaar/app/configs/SecurityConfig.java b/src/main/java/org/ayfaar/app/configs/SecurityConfig.java new file mode 100644 index 00000000..4d42617a --- /dev/null +++ b/src/main/java/org/ayfaar/app/configs/SecurityConfig.java @@ -0,0 +1,51 @@ +package org.ayfaar.app.configs; + +import org.ayfaar.app.model.User; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/static/old/adm.html") + .hasAnyAuthority("ROLE_EDITOR", "ROLE_ADMIN") + .and().logout().logoutSuccessUrl("/") + .and().csrf().disable(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(new CustomAuthenticationProvider()); + } + + @Component + public static class CustomAuthenticationProvider implements AuthenticationProvider { + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + User user = (User)authentication.getPrincipal(); + Collection authorities = AuthorityUtils.createAuthorityList(user.getRole().toString()); + return new UsernamePasswordAuthenticationToken(user, null, authorities); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } +} diff --git a/src/main/java/org/ayfaar/app/configs/WebMvcConfig.java b/src/main/java/org/ayfaar/app/configs/WebMvcConfig.java new file mode 100644 index 00000000..acda1185 --- /dev/null +++ b/src/main/java/org/ayfaar/app/configs/WebMvcConfig.java @@ -0,0 +1,85 @@ +package org.ayfaar.app.configs; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.resource.AppCacheManifestTransformer; +import org.springframework.web.servlet.resource.PathResourceResolver; +import org.springframework.web.servlet.resource.ResourceResolverChain; +import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Configuration +@AutoConfigureAfter(DispatcherServletAutoConfiguration.class) +public class WebMvcConfig extends WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter { + + @Autowired + private Environment env; + + @Value("${sitemap-dir}") + private String sitemapDir; + + @Bean + public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { + return new ResourceUrlEncodingFilter(); + } + + @Bean OncePerRequestFilter chromeTabSupportFilter() { + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Matcher matcher = Pattern + .compile("index\\.php\\?option=com_search&searchword=(.*)") + .matcher(request.getRequestURI() + "?" + request.getQueryString()); + if (matcher.find()) { + response.sendRedirect(matcher.group(1).replace("+", "%20")); + } else { + filterChain.doFilter(request, response); + } + } + }; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + boolean devMode = this.env.acceptsProfiles("dev"); + boolean useResourceCache = !devMode; + Integer cachePeriod = devMode ? 0 : null; + + registry.addResourceHandler("/**") + .addResourceLocations("classpath:static/", "file:"+sitemapDir) + .setCachePeriod(cachePeriod) + .resourceChain(useResourceCache) + .addResolver(new CustomPathResourceResolver()) + .addTransformer(new AppCacheManifestTransformer()); + } + + private class CustomPathResourceResolver extends PathResourceResolver { + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, List locations, ResourceResolverChain chain) { + if (requestPath.startsWith("template/") || requestPath.equals("sitemap.xml")) { + // don't change path + } else if (requestPath.startsWith("static/")) { + requestPath = requestPath.replaceFirst("static/", ""); + } else { + requestPath = "index.html"; + } + return super.resolveResource(request, requestPath, locations, chain); + } + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/ArticleController.java b/src/main/java/org/ayfaar/app/controllers/ArticleController.java index 352d80bc..75470d13 100644 --- a/src/main/java/org/ayfaar/app/controllers/ArticleController.java +++ b/src/main/java/org/ayfaar/app/controllers/ArticleController.java @@ -3,7 +3,8 @@ import org.ayfaar.app.dao.ArticleDao; import org.ayfaar.app.model.Article; import org.ayfaar.app.model.Term; -import org.ayfaar.app.utils.AliasesMap; +import org.ayfaar.app.utils.TermServiceImpl; +import org.ayfaar.app.utils.TermsMarker; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; @@ -15,18 +16,19 @@ import static org.springframework.util.Assert.notNull; @Controller -@RequestMapping("article") +@RequestMapping("api/article") public class ArticleController { @Autowired ArticleDao articleDao; - @Autowired AliasesMap aliasesMap; - + @Autowired TermServiceImpl aliasesMap; + @Autowired TermsMarker termsMarker; @RequestMapping("{id}") @ResponseBody public Article get(@PathVariable Integer id) { Article article = articleDao.get("id", id); notNull(article, "Article not found"); + article.setContent(termsMarker.mark(article.getContent())); return article; } diff --git a/src/main/java/org/ayfaar/app/controllers/AuthController.java b/src/main/java/org/ayfaar/app/controllers/AuthController.java new file mode 100644 index 00000000..60508f8a --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/AuthController.java @@ -0,0 +1,98 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.configs.SecurityConfig; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; + +@RestController +@RequestMapping("api/auth") +public class AuthController { + + private final CommonDao commonDao; + private ModerationService moderationService; + private final SecurityConfig.CustomAuthenticationProvider customAuthenticationProvider; + + @Inject + public AuthController(SecurityConfig.CustomAuthenticationProvider customAuthenticationProvider, CommonDao commonDao, ModerationService moderationService) { + this.customAuthenticationProvider = customAuthenticationProvider; + this.commonDao = commonDao; + this.moderationService = moderationService; + } + + @RequestMapping(method = RequestMethod.POST) + /* + Регистрируем нового пользователя и/или (если такой уже есть) назначаем его текущим для этой сессии + + Пример входных данных: + access_token:CAANCEx9hQ8ABACe5zBAPE1fThMsaJDHQ0oolOvZCsiOAoFgbj65BiZC5qFG557wYl71CRLZBBipi1JeZCZABkeD7PuurKplra04wvaGSiNnHdnWQZAqZBt1sLtps38DDOJ0RAUNlSDKnMjAkt7bZClUtxLCCF1lQk4NLIXMtuxXiKkLCnojk7KtoQbZBRbPTqzdadfbifnGUrOAZDZD + email:sllouyssgort@gmail.com + first_name:Sllouyssgort + id:1059404344124694 + last_name:Smaay-Grriyss + name:Sllouyssgort Smaay-Grriyss + picture:https://graph.facebook.com/1059404344124694/picture + thumbnail:https://graph.facebook.com/1059404344124694/picture + timezone:3 + verified:true + auth_provider:vk + */ + public User registrate(@RequestParam String access_token, + @RequestParam String email, + @RequestParam String first_name, + @RequestParam String last_name, + @RequestParam String name, + @RequestParam String picture, + @RequestParam String thumbnail, + @RequestParam(required=false) String timezone, + @RequestParam Long id, + @RequestParam OAuthProvider auth_provider) throws IOException{ + User user = commonDao.getOpt(User.class, "email", email).orElse( + User.builder() + .accessToken(access_token) + .email(email) + .oauthProvider(auth_provider) + .firstName(first_name) + .lastName(last_name) + .name(name) + .thumbnail(thumbnail) + .picture(picture) + .timezone(timezone) + .providerId(id) + .build()); + + boolean newUserFlag = user.getId() == null; + + if (!user.getAccessToken().equals(access_token)){ + user.setAccessToken(access_token); + } + user.setLastVisitAt(new Date()); + commonDao.save(user); + + Authentication request = new UsernamePasswordAuthenticationToken(user, null); + Authentication authentication = customAuthenticationProvider.authenticate(request); + SecurityContextHolder.getContext().setAuthentication(authentication); + if (newUserFlag) moderationService.notice(Action.NEW_USER, user.getName()); + return user; + } + + @RequestMapping("login-as/{userId}") + public void loginAs(@PathVariable Integer userId, HttpServletResponse response) throws IOException{ + User user = commonDao.getOpt(User.class, userId).get(); + + Authentication request = new UsernamePasswordAuthenticationToken(user, null); + Authentication authentication = customAuthenticationProvider.authenticate(request); + SecurityContextHolder.getContext().setAuthentication(authentication); + response.sendRedirect("/%D1%8F"); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/CategoryController.java b/src/main/java/org/ayfaar/app/controllers/CategoryController.java index da5a2ad7..38f2831b 100644 --- a/src/main/java/org/ayfaar/app/controllers/CategoryController.java +++ b/src/main/java/org/ayfaar/app/controllers/CategoryController.java @@ -5,6 +5,9 @@ import org.ayfaar.app.dao.LinkDao; import org.ayfaar.app.dao.TermDao; import org.ayfaar.app.model.Category; +import org.ayfaar.app.utils.ContentsService; +import org.ayfaar.app.utils.UriGenerator; +import org.ayfaar.app.utils.contents.ContentsHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,12 +21,23 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; @Controller -@RequestMapping("category") +@RequestMapping("api/category") public class CategoryController { @Autowired CommonDao commonDao; @Autowired TermDao termDao; @Autowired LinkDao linkDao; @Autowired ItemDao itemDao; + @Autowired ContentsHelper contentsHelper; + @Autowired ContentsService contentsService; + +// @ContentsCache + @RequestMapping + @ResponseBody + public Object getContents(@RequestParam("name") String name) { + name = UriGenerator.getValueFromUri(Category.class, name); + name = name.replace("параграф:", ""); + return contentsHelper.createContents(name); + } @RequestMapping(value = "add", method = POST) @ResponseBody @@ -55,4 +69,9 @@ public List autoComplete(@RequestParam("filter[filters][0][value]") Stri } return names; } + + @RequestMapping("reload") + public void reload() { + contentsService.reload(); + } } diff --git a/src/main/java/org/ayfaar/app/controllers/DocumentController.java b/src/main/java/org/ayfaar/app/controllers/DocumentController.java new file mode 100644 index 00000000..f836b21d --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/DocumentController.java @@ -0,0 +1,85 @@ +package org.ayfaar.app.controllers; + +import com.google.api.services.drive.model.File; +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.annotations.Moderated; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Document; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.utils.GoogleService; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.ayfaar.app.utils.UriGenerator.generate; +import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.util.Assert.hasLength; + +@Slf4j +@RestController +@RequestMapping("api/document") +public class DocumentController { + @Inject CommonDao commonDao; + @Inject GoogleService googleService; + @Inject ModerationService moderationService; + + @RequestMapping(method = RequestMethod.POST) + @Moderated(value = Action.DOCUMENT_ADD, command = "@documentController.create") + public Document create(@RequestParam String url, + @RequestParam(required = false) Optional name, + @RequestParam(required = false) String author, + @RequestParam(required = false) String annotation) throws IOException { + Assert.hasLength(url); + if(!url.contains("google.com")){ + File file = googleService.uploadToGoogleDrive(url, name.orElse("Новый документ")); + url = file.getAlternateLink(); + } + final String docId = GoogleService.extractDocIdFromUrl(url); + return commonDao.getOpt(Document.class, generate(Document.class, docId)) + .orElseGet(() -> { + final GoogleService.DocInfo docInfo = googleService.getDocInfo(docId); + final Document document = Document.builder() + .id(docId) + .name(name.orElse(docInfo.title)) + .annotation(annotation) + .author(author) + .thumbnail(docInfo.thumbnailLink) + .mimeType(docInfo.mimeType) + .icon(docInfo.iconLink) + .downloadUrl(docInfo.downloadUrl) + .build(); + commonDao.save(document); + moderationService.notice(Action.DOCUMENT_CREATED, document.getName(), document.getUri()); + return document; + }); + + } + + @RequestMapping("{id}") + public Document get(@PathVariable String id) { + return commonDao.get(Document.class, generate(Document.class, id)); + } + + @RequestMapping("last") + public List getLast(@PageableDefault(size = 9, sort = "createdAt", direction = DESC) Pageable pageable) { + return commonDao.getPage(Document.class, pageable); + } + + @RequestMapping(value = "update-name", method = RequestMethod.POST) + @Moderated(value = Action.DOCUMENT_RENAME, command = "@documentController.updateTitle") + public void updateTitle(@RequestParam String uri, @RequestParam String title) { + hasLength(uri); + Document document = commonDao.getOpt(Document.class, "uri", uri).orElseThrow(() -> new RuntimeException("Couldn't rename, document is not defined!")); + final String oldName = document.getName(); + document.setName(title); + commonDao.save(document); + moderationService.notice(Action.DOCUMENT_RENAMED, oldName, document.getName(), document.getUri()); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/ImageController.java b/src/main/java/org/ayfaar/app/controllers/ImageController.java new file mode 100644 index 00000000..b165e0cf --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/ImageController.java @@ -0,0 +1,160 @@ +package org.ayfaar.app.controllers; + + +import com.google.api.services.drive.model.File; +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Image; +import org.ayfaar.app.services.images.ImageService; +import org.ayfaar.app.services.links.LinkService; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.utils.GoogleService; +import org.ayfaar.app.utils.SearchSuggestions; +import org.dozer.Mapper; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.*; + +import static java.lang.Math.min; +import static org.ayfaar.app.utils.UriGenerator.generate; +import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.util.Assert.hasLength; + +@Slf4j +@RestController +@RequestMapping("api/image") +public class ImageController { + @Inject CommonDao commonDao; + @Inject GoogleService googleService; + @Inject ImageService imageService; + @Inject LinkService linkService; + @Inject SearchSuggestions searchSuggestions; + @Inject TopicService topicService; + @Inject Mapper mapper; + + private static final int MAX_SUGGESTIONS = 7; + + @RequestMapping(method = RequestMethod.POST) + public Image create(@RequestParam String url, + @RequestParam(required = false) Optional name) throws IOException { + + Assert.hasLength(url); + if(!url.contains("google.com")){ + File file = googleService.uploadToGoogleDrive(url, name.orElse("Новая иллюстрация")); + url = file.getAlternateLink(); + } + final String imgId = GoogleService.extractImageIdFromUrl(url); + return commonDao.getOpt(Image.class,generate(Image.class,imgId)) + .orElseGet(() -> { + final GoogleService.ImageInfo imageInfo = googleService.getImageInfo(imgId); + final Image image = Image.builder() + .id(imgId) + .name(name.orElse(imageInfo.title)) + .downloadUrl(imageInfo.downloadUrl) + .mimeType(imageInfo.mimeType) + .thumbnail(imageInfo.thumbnailLink) + .build(); + commonDao.save(image); + imageService.registerImage(image); + return image; + }); + } + + @RequestMapping() + public List getAll() { + return imageService.getAllImages(); + } + + @RequestMapping("{id}") + public Image get(@PathVariable String id) { + return commonDao.get(Image.class, generate(Image.class, id)); + } + + @RequestMapping("last") + public List getLast(@PageableDefault(size = 9, sort = "createdAt", direction = DESC) Pageable pageable) { + return commonDao.getPage(Image.class, pageable); + } + + @RequestMapping(value = "update-name", method = RequestMethod.POST) + public void updateTitle(@RequestParam String uri, @RequestParam String title) { + hasLength(uri); + Image image = commonDao.getOpt(Image.class, "uri", uri).orElseThrow(() -> new RuntimeException("Couldn't rename, image is not defined!")); + image.setName(title); + commonDao.save(image); + } + + @RequestMapping("{id}/remove") + public void remove(@PathVariable String id) { + commonDao.getOpt(Image.class, "id", id).ifPresent(image -> { + imageService.removeImage(image); + commonDao.remove(image); + }); + } + + @RequestMapping(value = "update-comment", method = RequestMethod.POST) + public void updateComment(@RequestParam String uri, @RequestParam String comment) { + hasLength(uri); + Image image = commonDao.getOpt(Image.class, "uri", uri).orElseThrow(() -> new RuntimeException("Couldn't update comment, image is not defined!")); + image.setComment(comment); + commonDao.save(image); + } + + @RequestMapping("search") + public List search(@RequestParam String q){ + Map allSuggestions = new LinkedHashMap<>(); + List items = new ArrayList<>(); + items.add(Suggestions.IMAGES); + items.add(Suggestions.TOPIC); //image-keywords + for (Suggestions item : items) { + Queue queriesQueue = searchSuggestions.getQueue(q); + List> suggestions = getSuggestions(queriesQueue, item); + allSuggestions.putAll(searchSuggestions.getAllSuggestions(q,suggestions)); + } + + return StreamEx.of(allSuggestions.entrySet()) + .map(entry -> { + final ImageEx imageEx = mapper.map(imageService.getByUri(entry.getKey()), ImageEx.class); + if (!entry.getValue().equals(imageEx.getName().toLowerCase())) imageEx.hint = entry.getValue(); + return imageEx; + }) + .toList(); + } + + private List> getSuggestions(Queue queriesQueue, Suggestions item) { + List> suggestions = new ArrayList<>(); + + while (suggestions.size() < MAX_SUGGESTIONS && queriesQueue.peek() != null) { + List> founded = null; + Map mapUriWithNames = null; + + switch (item) { + case TOPIC://image-keywords + mapUriWithNames = imageService.getImagesKeywords(); + break; + case IMAGES: + mapUriWithNames = imageService.getAllUriNames(); + break; + } + + founded = searchSuggestions.getSuggested(queriesQueue.poll(), suggestions, mapUriWithNames.entrySet(), Map.Entry::getValue); + + suggestions.addAll(founded.subList(0, min(MAX_SUGGESTIONS - suggestions.size(), founded.size()))); + } + + Collections.sort(suggestions, (e1, e2) -> Integer.valueOf(e1.getValue().length()).compareTo(e2.getValue().length())); + return suggestions; + } + + public static class ImageEx extends Image { + public String hint; + + public ImageEx() { + } + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/IntegrationController.java b/src/main/java/org/ayfaar/app/controllers/IntegrationController.java new file mode 100644 index 00000000..45bb19af --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/IntegrationController.java @@ -0,0 +1,75 @@ +package org.ayfaar.app.controllers; + +import org.apache.commons.lang.ArrayUtils; +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.utils.RegExpUtils; +import org.ayfaar.app.utils.TermService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.net.URLDecoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.compile; + +@Controller +@RequestMapping("api/integration") +public class IntegrationController { + @Autowired + TermService termService; + @Autowired ItemDao itemDao; + + private List allItemNumbers; + private Map>> cache = new HashMap>>(); + private String[] ignoreTerms = {"интеллект", "чувство", "мысль", "воля", "время", "мир", "личность", "жизнь", "ген", + "молекула", "атом", "элементарный", "форма", "окружающая действительность", "днк", + "трансформация", "сознание", "мироздание", "закон", "реальность", "энергия", "истина", "масса", "вселенная", + "идеи", "разум", "масса", "инерция", "земля", "аспект", "орис", "осознанность", "любовь", "пространство", + "мудрость" + }; + + @RequestMapping + @ResponseBody + public Object t(@RequestBody String text, @RequestHeader("Referer") String referer, @RequestParam String id) { + String cacheKey = referer+"#"+id; + if (cache.containsKey(cacheKey)) return cache.get(cacheKey); + + Map contains = new LinkedHashMap(); + text = URLDecoder.decode(text).toLowerCase(); + + // terms + for (Map.Entry entry : termService.getAll()) { + String key = entry.getKey(); + TermService.TermProvider provider = entry.getValue(); + if (ArrayUtils.contains(ignoreTerms, provider.getName().toLowerCase())) continue; + Matcher matcher = compile("((" + RegExpUtils.W + ")|^)" + key + "((" + RegExpUtils.W + ")|$)", Pattern.UNICODE_CHARACTER_CLASS).matcher(text); + if (matcher.find()) { + contains.put(key, provider.getName()); + text = text.replaceAll(key, ""); + } + } + // item numbers + if (allItemNumbers == null) { + allItemNumbers = itemDao.getAllNumbers(); + } + for (String itemNumber : allItemNumbers) { + Matcher matcher = compile("((" + RegExpUtils.W + ")|^|\\[)" + itemNumber + "((" + RegExpUtils.W + ")|$|\\])", Pattern.UNICODE_CHARACTER_CLASS).matcher(text); + if (matcher.find()) { + contains.put(itemNumber, itemNumber); + } + } + + final List> list = new ArrayList>(contains.entrySet()); + Collections.sort(list, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return Integer.compare(o2.getKey().length(), o1.getKey().length()); + } + }); + cache.put(cacheKey, list); + return list.isEmpty() ? "" : list; + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/ItemController.java b/src/main/java/org/ayfaar/app/controllers/ItemController.java index a29dff2d..9102bf4a 100644 --- a/src/main/java/org/ayfaar/app/controllers/ItemController.java +++ b/src/main/java/org/ayfaar/app/controllers/ItemController.java @@ -6,8 +6,11 @@ import org.ayfaar.app.model.Item; import org.ayfaar.app.model.Link; import org.ayfaar.app.model.Term; -import org.ayfaar.app.spring.Model; -import org.ayfaar.app.utils.AliasesMap; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.TermServiceImpl; +import org.ayfaar.app.utils.TermsTaggingUpdater; +import org.ayfaar.app.utils.exceptions.ExceptionCode; +import org.ayfaar.app.utils.exceptions.LogicalException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -16,22 +19,27 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + import static org.ayfaar.app.utils.ValueObjectUtils.getModelMap; import static org.springframework.util.Assert.notNull; import static org.springframework.web.bind.annotation.RequestMethod.POST; @Controller -@RequestMapping("item") +@RequestMapping("api/item") public class ItemController { @Autowired CommonDao commonDao; @Autowired ItemDao itemDao; @Autowired TermDao termDao; @Autowired TermController termController; - @Autowired AliasesMap aliasesMap; + @Autowired TermService termService; + @Autowired TermServiceImpl aliasesMap; + @Autowired TermsTaggingUpdater taggingUpdater; @RequestMapping(value = "{number}", method = POST) - @Model + @ResponseBody public Item add(@PathVariable String number, @RequestBody String content) { Item item = itemDao.getByNumber(number); if (item == null) { @@ -42,11 +50,17 @@ public Item add(@PathVariable String number, @RequestBody String content) { return item; } + @RequestMapping("{number}!") + public void update(@PathVariable String number, HttpServletResponse response) throws IOException { + taggingUpdater.update(itemDao.getByNumber(number)); + response.sendRedirect(number); + } + @RequestMapping("{number}") - @Model + @ResponseBody public ModelMap get(@PathVariable String number) { Item item = itemDao.getByNumber(number); - notNull(item, "Item not found"); + if (item == null) throw new LogicalException(ExceptionCode.ITEM_NOT_FOUND, number); ModelMap modelMap = (ModelMap) getModelMap(item); // modelMap.put("linkedTerms", getLinkedTerms(item)); Item next = itemDao.get(item.getNext()); @@ -86,7 +100,6 @@ public static String formatNumber(int number, int length) { } @RequestMapping("{number}/linked-terms") - @Model @ResponseBody public Object getLinkedTerms(@PathVariable String number) { Item item = itemDao.getByNumber(number); @@ -96,13 +109,13 @@ public Object getLinkedTerms(@PathVariable String number) { } @RequestMapping("{number}/{term}") - @Model + @ResponseBody public Link assignToTerm(@PathVariable String number, @PathVariable String term) { return assignToTermWithWeight(number, term, null); } @RequestMapping("{number}/{term}/{weight}") - @Model + @ResponseBody public Link assignToTermWithWeight(@PathVariable String number, @PathVariable String term, @PathVariable Byte weight) { diff --git a/src/main/java/org/ayfaar/app/controllers/LinkController.java b/src/main/java/org/ayfaar/app/controllers/LinkController.java index 4323c128..9e33f39d 100644 --- a/src/main/java/org/ayfaar/app/controllers/LinkController.java +++ b/src/main/java/org/ayfaar/app/controllers/LinkController.java @@ -8,31 +8,35 @@ import org.ayfaar.app.model.Link; import org.ayfaar.app.model.Term; import org.ayfaar.app.model.TermMorph; -import org.ayfaar.app.utils.AliasesMap; +import org.ayfaar.app.services.links.LinkService; import org.ayfaar.app.utils.EmailNotifier; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.TermsMarker; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; import javax.mail.MessagingException; - import java.util.List; +import static org.springframework.data.domain.Sort.Direction.DESC; import static org.springframework.web.bind.annotation.RequestMethod.POST; -@Controller -@RequestMapping("link") +@RestController +@RequestMapping("api/link") public class LinkController { @Autowired LinkDao linkDao; @Autowired TermDao termDao; @Autowired ItemDao itemDao; @Autowired TermController termController; @Autowired EmailNotifier emailNotifier; - @Autowired AliasesMap aliasesMap; + @Autowired TermService termService; @Autowired TermMorphDao termMorphDao; + @Autowired TermsMarker termsMarker; +// @Autowired ApplicationEventPublisher eventPublisher; + @Autowired LinkService linkService; + @RequestMapping(value = "addQuote", method = POST) @ResponseBody @@ -44,16 +48,17 @@ public Integer link(@RequestParam("term") String termName, } Term term = termDao.getByName(termName); if (term == null) { - if (aliasesMap.get(termName) != null) { - term = aliasesMap.get(termName).getTerm(); + if (termService.get(termName) != null) { + term = termService.getTerm(termName); } else { term = termController.add(termName); } } Item item = itemDao.getByNumber(itemNumber); - Link link = linkDao.save(new Link(term, item, quote.isEmpty() ? null : quote)); - - emailNotifier.newQuoteLink(term.getName(), itemNumber, quote, link.getLinkId()); + Link link = linkDao.save(new Link(term, item, + quote.isEmpty() ? null : quote, + quote.isEmpty() ? null : termsMarker.mark(quote))); + linkService.registerNew(link); return link.getLinkId(); } @@ -70,6 +75,8 @@ public Integer addAlias(@RequestParam("term1") String term, if (type != null && type == 0) { // Morph termMorphDao.save(new TermMorph(alias, primTerm.getUri())); + // need it to start tagging update for this term and all morph aliases +// eventPublisher.publishEvent(new TermUpdatedEvent(primTerm, alias)); return 1; } Term aliasTerm = termDao.getByName(alias); @@ -78,7 +85,9 @@ public Integer addAlias(@RequestParam("term1") String term, } Link link = linkDao.save(new Link(primTerm, aliasTerm, type)); - emailNotifier.newLink(term, alias, link.getLinkId()); + //emailNotifier.newLink(term, alias, link.getLinkId()); +// eventPublisher.publishEvent(new NewLinkEvent(term, alias, link)); + linkService.registerNew(link); return link.getLinkId(); } @@ -93,4 +102,35 @@ public void remove(@PathVariable Integer id) { public List getCreatedFromSearch(){ return linkDao.getList("source", "search"); } + + @RequestMapping("last") + public List getLast(@PageableDefault(size = 10, sort = "createdAt", direction = DESC) Pageable pageable) { + return linkDao.getPage(pageable); + } + + @RequestMapping("update/rate") + public void updateRate(@RequestParam String uri1, + @RequestParam String uri2, + @RequestParam Float value) { + linkService.getByUris(uri1, uri2).get().updater().rate(value).commit(); + } + + @RequestMapping("update/comment") + public void updateComment(@RequestParam String uri1, + @RequestParam String uri2, + @RequestParam String value) { + linkService.getByUris(uri1, uri2).get().updater().comment(value).commit(); + } + + @RequestMapping("update/quote") + public void updateQuote(@RequestParam String uri1, + @RequestParam String uri2, + @RequestParam String value) { + linkService.getByUris(uri1, uri2).get().updater().quote(value).commit(); + } + + @RequestMapping("reload") + public void reload() { + linkService.reload(); + } } diff --git a/src/main/java/org/ayfaar/app/controllers/MaintenanceController.java b/src/main/java/org/ayfaar/app/controllers/MaintenanceController.java new file mode 100644 index 00000000..48d4e64e --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/MaintenanceController.java @@ -0,0 +1,47 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.services.EntityLoader; +import org.ayfaar.app.sync.GetVideosFormYoutube; +import org.ayfaar.app.sync.RecordSynchronizer; +import org.ayfaar.app.sync.VocabularySynchronizer; +import org.ayfaar.app.translation.TopicTranslationSynchronizer; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.io.IOException; + +@RestController +@RequestMapping("api") +public class MaintenanceController { + @Inject EntityLoader entityLoader; + @Inject RecordSynchronizer recordSynchronizer; + @Inject TopicTranslationSynchronizer topicTranslationSynchronizer; + @Inject GetVideosFormYoutube getVideosFormYoutube; + @Inject VocabularySynchronizer vocabularySynchronizer; + + @RequestMapping("entity-loader/clear") + public void clearEntityLoader() { + entityLoader.clear(); + } + + @RequestMapping("sync/records") + public void synchronizeRecords() throws IOException { + recordSynchronizer.synchronize(); + } + + @RequestMapping("sync/translations") + public void synchronizeTranslations() throws IOException { + topicTranslationSynchronizer.synchronize(); + } + + @RequestMapping("sync/videos") + public void synchronizeVideos() throws IOException { + getVideosFormYoutube.synchronize(); + } + + @RequestMapping("sync/vocabulary") + public void synchronizeVocabulary() throws IOException { + vocabularySynchronizer.synchronize(); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/ModerationController.java b/src/main/java/org/ayfaar/app/controllers/ModerationController.java new file mode 100644 index 00000000..ab24f4d9 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/ModerationController.java @@ -0,0 +1,97 @@ +package org.ayfaar.app.controllers; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.ActionEvent; +import org.ayfaar.app.model.PendingAction; +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.services.user.UserService; +import org.ayfaar.app.utils.CurrentUserProvider; +import org.hibernate.criterion.Restrictions; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.util.List; +import java.util.Objects; + +import static java.util.Comparator.comparingInt; +import static org.springframework.data.domain.Sort.Direction.DESC; + +@RestController +@RequestMapping("api/moderation") +@PreAuthorize("authenticated") +public class ModerationController { + private final CommonDao commonDao; + private final ModerationService service; + private final UserService userService; + private CurrentUserProvider currentUserProvider; + + @Inject + public ModerationController(ModerationService service, CommonDao commonDao, UserService userService, CurrentUserProvider currentUserProvider) { + this.service = service; + this.commonDao = commonDao; + this.userService = userService; + this.currentUserProvider = currentUserProvider; + } + + @RequestMapping("pending_actions") + public List getPendingActions(@AuthenticationPrincipal User currentUser) { + // show only my users (this user can be linked with another as children for personal moderation) + return StreamEx.of(commonDao.getList(PendingAction.class, "confirmedBy", null)) + .filter(a -> currentUserProvider.getCurrentAccessLevel().accept(a.getAction().getRequiredAccessLevel()) + || Objects.equals(currentUser.getId(), a.getInitiatedBy())) + .reverseSorted(comparingInt(PendingAction::getId)) + .map(PendingActionPresentation::new) + .map(presentation -> { + presentation.owner = Objects.equals(currentUser.getId(), presentation.initiatedBy); + return presentation; + }) + .toList(); + } + + @RequestMapping("last_actions") + @Transactional + public List getLastActions(@PageableDefault(sort = "createdAt", direction = DESC) Pageable pageable, + @AuthenticationPrincipal User currentUser) { + Integer hiddenActionEventId = currentUser.getHiddenActionEventId(); + if (hiddenActionEventId == null) hiddenActionEventId = 0; + //noinspection unchecked + return commonDao.getCriteria(ActionEvent.class, pageable) + .add(Restrictions.ne("createdBy", currentUser.getId())) + .add(Restrictions.gt("id", hiddenActionEventId)) + .list(); + } + + @RequestMapping("{id}/confirm") + public void confirm(@PathVariable Integer id) { + final PendingAction action = commonDao.getOpt(PendingAction.class, id).get(); + service.confirm(action); + } + + @RequestMapping(value = "{id}/cancel", method = RequestMethod.POST) + public void cancel(@PathVariable Integer id) { + service.cancel(id); + } + + private class PendingActionPresentation { + public Integer id; + public String text; + public Integer initiatedBy; + public Boolean owner; // is this action created by current user + + public PendingActionPresentation(PendingAction action) { + id = action.getId(); + text = action.getMessage(); + initiatedBy = action.getInitiatedBy();//userService.getPresentation(action.getInitiatedBy()); + } + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/NewItemController.java b/src/main/java/org/ayfaar/app/controllers/NewItemController.java new file mode 100644 index 00000000..35d22e96 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/NewItemController.java @@ -0,0 +1,163 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.utils.ContentsService; +import org.ayfaar.app.utils.ContentsService.ParagraphProvider; +import org.ayfaar.app.utils.TermsMarker; +import org.ayfaar.app.utils.TermsTaggingUpdater; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static org.ayfaar.app.controllers.ItemController.getPrev; +import static org.ayfaar.app.utils.UriGenerator.generate; +import static org.springframework.util.Assert.notNull; +import static org.springframework.util.StringUtils.isEmpty; + +@Controller +@RequestMapping("api/v2/item") +public class NewItemController { + + private static final int MAXIMUM_RANGE_SIZE = 200; + @Inject ItemDao itemDao; + @Inject ContentsService contentsService; + @Inject TermsMarker termsMarker; + @Inject AsyncTaskExecutor taskExecutor; + @Inject TermsTaggingUpdater taggingUpdater; + + @RequestMapping + @ResponseBody + public ItemPresentation get(@RequestParam String number) { + final Item item = itemDao.getByNumber(number); + notNull(item, format("Item `%s` not found", number)); + + if (item.getNext() != null) { + taskExecutor.execute(() -> { + final Item nextItem = itemDao.get(item.getNext()); + taggingUpdater.update(nextItem); + }); + } + if (isEmpty(item.getTaggedContent())) { + taggingUpdater.update(item); + itemDao.save(item); + } + return new ItemPresentation(item, generate(Item.class, getPrev(item.getNumber())), true); + } + + @RequestMapping("{number}/{more}more") + @ResponseBody + public List getMore(@PathVariable String number, @PathVariable Integer more) { + List presentations = new ArrayList(); + + for (Item item : itemDao.getNext(number, more)) { + presentations.add(new ItemPresentation(item)); + } + return presentations; + } + + @RequestMapping("range") + @ResponseBody + public List get(@RequestParam String from, @RequestParam String to) { + Item item = itemDao.getByNumber(from); + notNull(item, format("Item `%s` not found", from)); + notNull(itemDao.getByNumber(to), format("Item `%s` not found", to)); + + List items = new ArrayList(); + + final ItemPresentation itemPresentation = new ItemPresentation(item); +// itemPresentation.parents = parents(item.getNumber()); + items.add(itemPresentation); + + while (!item.getNumber().equals(to)) { + item = itemDao.get(item.getNext()); + items.add(new ItemPresentation(item)); + if (items.size() > MAXIMUM_RANGE_SIZE) { + throw new RuntimeException(format("Maximum range size reached (from %s to %s)", from, to)); + } + } + + return items; + } + + private class ItemPresentation { + public final String number; + public final String content; + public String uri; + public String next; + public String previous; + public List parents; + + public ItemPresentation(Item item, String previous, Boolean loadParents) { + this.uri = item.getUri(); + this.content = item.getTaggedContent(); + this.previous = previous; + number = item.getNumber(); + next = item.getNext(); + parents = loadParents ? getParents(item.getNumber()) : null; + } + public ItemPresentation(Item item) { + this(item, null, false); + } + } + + public class ParentPresentation { + public String from; + public String to; + public final String name; + public final String uri; + + public ParentPresentation(String name, String uri) { + this.name = name; + this.uri = uri; + } + + public ParentPresentation(String name, String uri, String from, String to) { + this(name, uri); + this.from = from; + this.to = to; + } + } + + @RequestMapping(value = "{number}/content", produces = "text/plain; charset=utf-8") + @ResponseBody + public String getContent(@PathVariable String number) { + Item item = itemDao.getByNumber(number); + if (item != null) { + return item.getTaggedContent(); + } + return null; + } + + @RequestMapping("{number}/mark") + public void mark(@PathVariable String number) { + Item item = itemDao.getByNumber(number); + if (item != null) { + item.setTaggedContent(termsMarker.mark(item.getContent())); + itemDao.save(item); + } + } + + private List getParents(String number) { + List parents = new ArrayList<>(); + Optional paragraphOpt = contentsService.getByItemNumber(number); + if (paragraphOpt.isPresent()) { + final ParagraphProvider paragraph = paragraphOpt.get(); + parents.addAll(paragraph.parents().stream() + .map(parent -> new ParentPresentation(parent.extractCategoryName(), parent.uri())) + .collect(Collectors.toList())); + parents.add(new ParentPresentation(paragraph.name(), paragraph.uri(), paragraph.from(), paragraph.to())); + } + return parents; + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/NewSearchController.java b/src/main/java/org/ayfaar/app/controllers/NewSearchController.java index 46ee4584..619bf000 100644 --- a/src/main/java/org/ayfaar/app/controllers/NewSearchController.java +++ b/src/main/java/org/ayfaar/app/controllers/NewSearchController.java @@ -1,115 +1,176 @@ package org.ayfaar.app.controllers; -import org.apache.commons.lang.NotImplementedException; +import lombok.Data; +import org.ayfaar.app.annotations.SearchResultCache; import org.ayfaar.app.controllers.search.Quote; import org.ayfaar.app.controllers.search.SearchQuotesHelper; import org.ayfaar.app.controllers.search.SearchResultPage; +import org.ayfaar.app.controllers.search.cache.DBCache; import org.ayfaar.app.dao.SearchDao; import org.ayfaar.app.model.Item; -import org.ayfaar.app.model.Term; +import org.ayfaar.app.services.itemRange.ItemRangeService; +import org.ayfaar.app.utils.ContentsService; +import org.ayfaar.app.utils.ContentsService.ContentsProvider; +import org.ayfaar.app.utils.RegExpUtils; +import org.ayfaar.app.utils.StringUtils; +import org.ayfaar.app.utils.TermService; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; -import java.util.List; +import java.io.IOException; +import java.util.*; import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.ayfaar.app.utils.TermService.TermProvider; -//todo пометить как контролер и зделать доступнім по адресу "v2/search" +@Controller +@RequestMapping("api/v2/search") public class NewSearchController { public static final int PAGE_SIZE = 20; - @Inject - private SearchQuotesHelper handleItems; - - @Inject - private SearchDao searchDao; - - private List searchQueries; - + @Inject SearchQuotesHelper handleItems; + @Inject SearchDao searchDao; + @Inject TermService termService; + @Inject ContentsService contentsService; + @Inject DBCache cache; + @Inject ItemRangeService itemRangeService; +// @Inject ApplicationEventPublisher eventPublisher; +// @Inject CacheUpdater cacheUpdater; /** * Поиск будет производить только по содержимому Item - * todo сделать этот метод доступным через веб * * @param pageNumber номер страницы */ - public SearchResultPage search(String query, Integer pageNumber, String fromItemNumber) { + @SearchResultCache + @RequestMapping + @ResponseBody + // возвращаем Object чтобы можно было вернуть закешированный json или SearchResultPage + public Object search(@RequestParam String query, + @RequestParam Integer pageNumber, + @RequestParam(required = false) String startFrom) { // 1. Очищаем введённую фразу от лишних пробелов по краям и переводим в нижний регистр query = prepareQuery(query); - // 2. Проверяем есть ли кеш, если да возвращаем его - if (hasCached(query, pageNumber, fromItemNumber)) { - return getCache(query, pageNumber, fromItemNumber); - } - SearchResultPage page = new SearchResultPage(); + page.setHasMore(false); // 3. Определить термин ли это - Term term = getTerm(query); - // если нет поискать в разных падежах - if (term == null) { - term = findTermInMorphs(query); - } - + Optional providerOpt = termService.get(query); // 3.1. Если да, Получить все синониме термина List foundItems; - // указывает сколько результатов поиска нужно пропустиьб, то есть когда ищем следующую страницу + // указывает сколько результатов поиска нужно пропустить, то есть когда ищем следующую страницу int skipResults = pageNumber*PAGE_SIZE; - if (term != null) { + List searchQueries; + if (providerOpt.isPresent()) { // 3.2. Получить все падежи по всем терминам - searchQueries = getAllMorphs(term); + searchQueries = providerOpt.get().getAllAliasesWithAllMorphs(); // 4. Произвести поиск - // 4.1. Сначала поискать совпадение термина в различных падежах - foundItems = searchDao.searchInDb(searchQueries, skipResults, PAGE_SIZE, fromItemNumber); - // 4.2. Если количества не достаточно для заполнения страницы то поискать по синонимам - List aliases = getAllAliases(term); - List aliasesSearchQueries = getAllMorphs(aliases); - foundItems.addAll(searchDao.searchInDb(searchQueries, skipResults, PAGE_SIZE - foundItems.size(), fromItemNumber)); - searchQueries.addAll(aliasesSearchQueries); + foundItems = searchDao.findInItems(searchQueries, skipResults, PAGE_SIZE + 1, startFrom); + +// if (foundItems.isEmpty()) { +// eventPublisher.publishEvent(new LinkPushEvent("Не найдено - "+provider.getName(), provider.getName())); +// } } else { // 4. Поиск фразы (не термин) - foundItems = searchDao.searchInDb(query, skipResults, PAGE_SIZE, fromItemNumber); + query = query.replace("!", ""); + searchQueries = asList(query.replace("%", "\\%").replace("*", "%")); + foundItems = searchDao.findInItems(searchQueries, skipResults, PAGE_SIZE + 1, startFrom); + searchQueries = asList(query.replace("%", "")); } - page.setHasMore(false); + if (foundItems.size() > PAGE_SIZE ) { + foundItems.remove(foundItems.size() - 1); + page.setHasMore(true); + } // 5. Обработка найденных пунктов List quotes = handleItems.createQuotes(foundItems, searchQueries); page.setQuotes(quotes); - // 6. Вернуть результат + // 7. Вернуть результат return page; } - private List getAllMorphs(Term term) { - return getAllMorphs(asList(term)); - } - private List getAllMorphs(List terms) { - throw new NotImplementedException(); - } + @RequestMapping("categories") + @ResponseBody + public Object inCategories(@RequestParam String query) { + final Optional providerOpt = termService.get(query); + List searchQueries; + if (providerOpt.isPresent()) { + TermProvider provider = providerOpt.get(); + provider = provider.getMainOrThis(); + searchQueries = provider.getAllAliasesAndAbbreviationsWithAllMorphs(); + } else { + query = query.replace("*", RegExpUtils.w + "+"); + searchQueries = Collections.singletonList(query); + } + List foundCategoryProviders = contentsService.descriptionContains(searchQueries); + + providerOpt + .ifPresent(termProvider -> itemRangeService.getParagraphsByMainTerm(termProvider.getMainOrThis().getName()) + .map(paragraphCode -> contentsService.getParagraph(paragraphCode)) + .filter(Optional::isPresent) + .map(Optional::get) + .sorted(Comparator.comparing(o -> Double.valueOf(o.from()))) + .forEachOrdered(foundCategoryProviders::add)); + + + List presentations = new ArrayList<>(); + for (ContentsService.ContentsProvider p : foundCategoryProviders) { + String strongMarkedDescription = StringUtils.markWithStrong(p.description(), searchQueries); + FoundCategoryPresentation presentation = new FoundCategoryPresentation(p.path(), p.uri(), strongMarkedDescription); + presentations.add(presentation); + } - private List getAllAliases(Term term) { - throw new NotImplementedException(); - } + if (presentations.size() > 20) { + // reduce result amount + presentations = presentations.stream() + .sorted(Comparator.comparingInt(o -> o.getDescription().indexOf("strong"))) + .limit(20) + .collect(toList()); + } - private Term findTermInMorphs(String query) { - throw new NotImplementedException(); + return presentations; } - private Term getTerm(String query) { - throw new NotImplementedException(); + private String prepareQuery(String query) { + if (query != null) { + query = query.replace("Обсуждение:", ""); + query = query.replace("_", " "); + query = query.toLowerCase().trim(); + } + return query; } - private SearchResultPage getCache(String query, Integer page, String fromItemNumber) { - throw new NotImplementedException(); + @RequestMapping("cache/clean") + public void cleanCache() { + cache.clear(); } - private boolean hasCached(String query, Integer page, String fromItemNumber) { - throw new NotImplementedException(); + @RequestMapping("cache/update") + public void updateCache() throws IOException { +// cacheUpdater.update(); } - private String prepareQuery(String query) { - throw new NotImplementedException(); + public Object searchWithoutCache(String query, Integer pageNumber, String fromItemNumber) { + return search(query, pageNumber, fromItemNumber); } + @Data + private class FoundCategoryPresentation { + private final String path; + private final String uri; + private final String description; + + public FoundCategoryPresentation(String path, String uri, String description) { + this.path = path; + this.uri = uri; + this.description = description; + } + } } diff --git a/src/main/java/org/ayfaar/app/controllers/NewSuggestionsController.java b/src/main/java/org/ayfaar/app/controllers/NewSuggestionsController.java new file mode 100644 index 00000000..0c6fd3fe --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/NewSuggestionsController.java @@ -0,0 +1,161 @@ +package org.ayfaar.app.controllers; + +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.ayfaar.app.dao.TermDao; +import org.ayfaar.app.model.Term; +import org.ayfaar.app.services.ItemService; +import org.ayfaar.app.services.document.DocumentService; +import org.ayfaar.app.services.images.ImageService; +import org.ayfaar.app.services.moderation.UserRole; +import org.ayfaar.app.services.record.RecordService; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.services.videoResource.VideoResourceService; +import org.ayfaar.app.utils.*; +import org.ayfaar.app.utils.contents.ContentsUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.util.*; +import java.util.stream.Collectors; + +import static java.lang.Math.min; + +@Slf4j +@RestController +@RequestMapping("api/suggestions") +public class NewSuggestionsController { + + @Inject TermService termService; + @Inject TopicService topicService; + @Inject ContentsService contentsService; + @Inject DocumentService documentService; + @Inject VideoResourceService videoResourceService; + @Inject RecordService recordService; + @Inject ItemService itemService; + @Inject ContentsUtils contentsUtils; + @Inject ImageService imageService; + @Inject SearchSuggestions searchSuggestions; + @Inject CurrentUserProvider currentUserProvider; + + private List escapeChars = Arrays.asList("(", ")", "[", "]", "{", "}"); + private static final int MAX_SUGGESTIONS = 5; + private static final int MAX_WORDS_PARAGRAPH_AFTER_SEARCH = 4; + + @RequestMapping("term") + @ResponseBody + public Collection suggestionTerms(@RequestParam String q) { + return suggestions(q, true, false, false, false, false, false, false, false, false, false, false) + .values(); + } + + @RequestMapping("all") + @ResponseBody + public Map suggestions(@RequestParam String q, + @RequestParam(required = false, defaultValue = "true") boolean with_terms, + @RequestParam(required = false, defaultValue = "true") boolean with_topic, + @RequestParam(required = false, defaultValue = "true") boolean with_category_name, + @RequestParam(required = false, defaultValue = "true") boolean with_category_description, + @RequestParam(required = false, defaultValue = "true") boolean with_doc, + @RequestParam(required = false, defaultValue = "true") boolean with_video, + @RequestParam(required = false, defaultValue = "true") boolean with_video_code, + @RequestParam(required = false, defaultValue = "true") boolean with_item, + @RequestParam(required = false, defaultValue = "true") boolean with_record_name, + @RequestParam(required = false, defaultValue = "true") boolean with_record_code, + @RequestParam(required = false, defaultValue = "true") boolean with_images + ) { + Map allSuggestions = new LinkedHashMap<>(); + List items = new ArrayList<>(); + if (with_terms) items.add(Suggestions.TERM); //default + if (with_topic) items.add(Suggestions.TOPIC); + if (with_category_name) items.add(Suggestions.CATEGORY_NAME); + if (with_category_description) items.add(Suggestions.CATEGORY_DESCRIPTION); + if (with_doc) items.add(Suggestions.DOCUMENT); + if (with_video) items.add(Suggestions.VIDEO); + if (with_video_code) items.add(Suggestions.VIDEO_CODE); + if (with_record_name) items.add(Suggestions.RECORD_NAME); + if (with_record_code) items.add(Suggestions.RECORD_CODE); + if (with_item) items.add(Suggestions.ITEM); + if (with_images) items.add(Suggestions.IMAGES); + for (Suggestions item : items) { + Queue queriesQueue = searchSuggestions.getQueue(q); + List> suggestions = getSuggestions(queriesQueue, item); + allSuggestions.putAll(searchSuggestions.getAllSuggestions(q,suggestions)); + } + + if (!currentUserProvider.getCurrentAccessLevel().accept(UserRole.ROLE_EDITOR)) { + // remove duplications by values, but not for editors and admins + Set existing = new HashSet<>(); + allSuggestions = allSuggestions.entrySet() + .stream() + .filter(entry -> existing.add(entry.getValue().toLowerCase())) + .collect(Collectors.toMap(Map.Entry::getKey, + Map.Entry::getValue, + (v1, v2) -> v1, + LinkedHashMap::new)); + } + + return allSuggestions; + } + + private List> getSuggestions(Queue queriesQueue, Suggestions item) { + List> suggestions = new ArrayList<>(); + + while (suggestions.size() < MAX_SUGGESTIONS && queriesQueue.peek() != null) { + List> founded = null; + Map mapUriWithNames = null; + // fixme: в некоторых методах getAllUriNames при каждом вызове getSuggestions, происходит запрос в БД для получения всех имён, это не рационально, я бы сделал логику кеширования имён и обновления кеша в случае добавления/изменения видео или документа + // или можно сделать RegExp запрос в БД и тогда не нужен кеш вовсе, просто из соображений скорости я стараюсь уменьшить запросы в БД + switch (item) { + case TERM: + Collection allInfoTerms = termService.getAllInfoTerms(); + final List suggested = searchSuggestions.getSuggested(queriesQueue.poll(), suggestions, allInfoTerms, TermDao.TermInfo::getName); + founded = StreamEx.of(suggested) + .map((i) -> new ImmutablePair<>(UriGenerator.generate(Term.class, i.getName()), i.getName())) + .toList(); + break; + case TOPIC: + mapUriWithNames = topicService.getAllUriNames(); + break; + case CATEGORY_NAME: + mapUriWithNames = contentsService.getAllUriNames(); + break; + case CATEGORY_DESCRIPTION: + mapUriWithNames = contentsService.getAllUriDescription(); + break; + case DOCUMENT: + mapUriWithNames = documentService.getAllUriNames(); + break; + case VIDEO: + mapUriWithNames = videoResourceService.getAllUriNames(); + break; + case VIDEO_CODE: + mapUriWithNames = videoResourceService.getAllUriCodes(); + break; + case ITEM: + mapUriWithNames = itemService.getAllUriNumbers(); + break; + case RECORD_NAME: + mapUriWithNames = recordService.getAllUriNames(); + break; + case RECORD_CODE: + mapUriWithNames = recordService.getAllUriCodes(); + break; + case IMAGES: + mapUriWithNames = imageService.getAllUriNames(); + break; + } + if (item != Suggestions.TERM) + founded = searchSuggestions.getSuggested(queriesQueue.poll(), suggestions, mapUriWithNames.entrySet(), Map.Entry::getValue); + + suggestions.addAll(founded.subList(0, min(MAX_SUGGESTIONS - suggestions.size(), founded.size()))); + } + + Collections.sort(suggestions, (e1, e2) -> Integer.valueOf(e1.getValue().length()).compareTo(e2.getValue().length())); + return suggestions; + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/controllers/NewTermController.java b/src/main/java/org/ayfaar/app/controllers/NewTermController.java new file mode 100644 index 00000000..ce359d2e --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/NewTermController.java @@ -0,0 +1,61 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.utils.RegExpUtils; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.TermServiceImpl; +import org.ayfaar.app.utils.TermsTaggingUpdater; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.sort; +import static java.util.regex.Pattern.compile; + +@RestController +@RequestMapping("api/v2/term") +public class NewTermController { + @Inject TermServiceImpl termsMap; + @Inject TermsTaggingUpdater taggingUpdater; + @Inject AsyncTaskExecutor taskExecutor; + @Inject SuggestionsController suggestionsController; + + @RequestMapping("{termName}/mark") + public void mark(@PathVariable final String termName) { + taskExecutor.submit(() -> taggingUpdater.update(termName)); + } + + @RequestMapping(value = "get-terms-in-text", method = RequestMethod.POST) + public Object getTerms(@RequestParam String text) { + Map contains = new HashMap<>(); + text = text.toLowerCase(); + + for (Map.Entry entry : termsMap.getAll()) { + String key = entry.getKey(); + Matcher matcher = compile("((" + RegExpUtils.W + ")|^)" + key + + "((" + RegExpUtils.W + ")|$)", Pattern.UNICODE_CHARACTER_CLASS) + .matcher(text); + if (matcher.find()) { + int count = 1; + while (matcher.find()) count++; + contains.put(entry.getValue().getName(), count); + text = text.replaceAll(key, ""); + } + } + + final List> sorted = new ArrayList>(contains.entrySet()); + sort(sorted, (o1, o2) -> o2.getValue().compareTo(o1.getValue())); + return sorted; + } + + @RequestMapping("suggest") + public List suggest(@RequestParam String q) { + return suggestionsController.suggestions(q); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/OAuthProvider.java b/src/main/java/org/ayfaar/app/controllers/OAuthProvider.java new file mode 100644 index 00000000..49c2b0ea --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/OAuthProvider.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.controllers; + +public enum OAuthProvider { + vk, facebook +} diff --git a/src/main/java/org/ayfaar/app/controllers/RecordController.java b/src/main/java/org/ayfaar/app/controllers/RecordController.java new file mode 100644 index 00000000..9b8966c2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/RecordController.java @@ -0,0 +1,133 @@ +package org.ayfaar.app.controllers; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.annotations.Moderated; +import org.ayfaar.app.dao.RecordDao; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.RecordRenamedEvent; +import org.ayfaar.app.model.Record; +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.services.moderation.UserRole; +import org.ayfaar.app.services.topics.TopicProvider; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.sync.RecordSynchronizer; +import org.ayfaar.app.utils.Transliterator; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +@RestController +@RequestMapping("api/record") +@Slf4j +public class RecordController { + + private final TopicService topicService; + private final ModerationService moderationService; + private final RecordSynchronizer recordSynchronizer; + private EventPublisher publisher; + private final RecordDao recordDao; + + @Inject + public RecordController(RecordDao recordDao, TopicService topicService, ModerationService moderationService, RecordSynchronizer recordSynchronizer, EventPublisher publisher) { + this.recordDao = recordDao; + this.topicService = topicService; + this.moderationService = moderationService; + this.recordSynchronizer = recordSynchronizer; + this.publisher = publisher; + } + + @RequestMapping() + public List> get(@RequestParam(required = false) String nameOrCode, + @RequestParam(required = false) String year, + @RequestParam(required = false) Record.Kind kind, + @RequestParam(required = false) Boolean with_url, + @AuthenticationPrincipal User currentUser, + @PageableDefault(sort = "recorderAt", direction = DESC, size = 30) Pageable pageable) { + with_url = with_url != null + ? with_url + : currentUser == null || !currentUser.getRole().accept(UserRole.ROLE_EDITOR); + List records = recordDao.get(nameOrCode, year, kind, with_url, pageable); + return records.stream().map(this::getRecordsInfo).collect(Collectors.toList()); + } + + private Map getRecordsInfo(Record record) { + Map recordsInfoMap = new HashMap<>(); + recordsInfoMap.put("code",record.getCode()); + recordsInfoMap.put("name",record.getName()); + recordsInfoMap.put("recorder_at",new SimpleDateFormat("yyyy-MM-dd").format(record.getRecorderAt())); + recordsInfoMap.put("url",record.getAudioUrl()); + recordsInfoMap.put("uri",record.getUri()); + recordsInfoMap.put("duration",record.getDuration()); + + List topicUris = topicService.getAllLinkedWith(record.getUri()) + .map(TopicProvider::name) + .collect(Collectors.toList()); + recordsInfoMap.put("topics", topicUris); + return recordsInfoMap; + } + + @RequestMapping(value = "{code}/rename", method = RequestMethod.POST) + @Moderated(value = Action.RECORD_RENAME, command = "@recordController.rename") + public void rename(@PathVariable String code, @RequestParam String name) { + final Record record = recordDao.get("code", code); + if (record == null) throw new RuntimeException("Record not found"); + + record.setPreviousName(record.getName()); + record.setName(name); + recordDao.save(record); + moderationService.notice(Action.RECORD_RENAMED, record.getUri(), record.getPreviousName(), record.getName()); + publisher.publishEvent(new RecordRenamedEvent(record)); + } + + @RequestMapping(value = "{code}/download/{any}", method = RequestMethod.GET) + public void download(@PathVariable String code, HttpServletResponse response) throws IOException{ + final Record record = recordDao.get("code", code); + if (record == null) throw new RuntimeException("Record not found"); + + final String url = record.getAudioUrl(); + if (url == null) throw new RuntimeException("Has no download url for record"); + + final int bufferSize = 4096; + final String mimeType = "audio/mpeg"; + String name = Transliterator.transliterate(record.getName()).replace("\"", ""); + String headerValue = String.format("attachment;"); + + InputStream inputStream = new URL(url).openStream(); + + response.setContentType(mimeType); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, headerValue); + + OutputStream outputStream = response.getOutputStream(); + byte[] buffer = new byte[bufferSize]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + outputStream.close(); +// return "redirect:" + url; + } + + @RequestMapping("sync") + public void sync() throws IOException { + recordSynchronizer.synchronize(); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/RobotsController.java b/src/main/java/org/ayfaar/app/controllers/RobotsController.java new file mode 100644 index 00000000..22b45e5d --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/RobotsController.java @@ -0,0 +1,20 @@ +package org.ayfaar.app.controllers; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping("/") +public class RobotsController { + + @Value("${site-base-url}") + private String baseUrl; + + @RequestMapping("robots.txt") + @ResponseBody + public String getRobots(){ + return "Sitemap: " + baseUrl + "/sitemap.xml"; + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/Router.java b/src/main/java/org/ayfaar/app/controllers/Router.java deleted file mode 100644 index e12b11a6..00000000 --- a/src/main/java/org/ayfaar/app/controllers/Router.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.ayfaar.app.controllers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.FileSystemResource; -import org.springframework.security.web.DefaultRedirectStrategy; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.IOException; - -@Controller -public class Router { - @Autowired ServletContext context; - private final DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - @Value("${OPENSHIFT_HOMEDIR}") - private String jbossDir; - - @RequestMapping("/") - @ResponseBody - public Object returnIndex(HttpServletRequest request, HttpServletResponse response) throws IOException { - String index = "index.html"; - String path = request.getServletContext().getRealPath(index); - if (path == null) { - path = jbossDir+"app-deployments/current/repo/src/main/webapp/"+index; - } - return new FileSystemResource(path); - -// File baseDir = new File(jbossDir); -// return find(baseDir); - } - - private String find(File dir) { - for (File file : dir.listFiles()) { - if (file.isDirectory() && file.canRead()) { - String result = find(file); - if (!result.equals("not found")) { - return result; - } - } else if (file.getName().equals("google9ff4abadde5fb24d.html")) { - return file.getAbsolutePath(); - } - } - return "not found"; - } - - /*@RequestMapping("/") - public void redirect(HttpServletRequest request, HttpServletResponse response) throws IOException { - redirectStrategy.setContextRelative(true); - redirectStrategy.sendRedirect(request, response, "index.html"); - }*/ -} diff --git a/src/main/java/org/ayfaar/app/controllers/SearchController.java b/src/main/java/org/ayfaar/app/controllers/SearchController.java index 2b952ee3..4719768c 100644 --- a/src/main/java/org/ayfaar/app/controllers/SearchController.java +++ b/src/main/java/org/ayfaar/app/controllers/SearchController.java @@ -1,40 +1,48 @@ package org.ayfaar.app.controllers; +import org.ayfaar.app.annotations.Moderated; import org.ayfaar.app.dao.*; import org.ayfaar.app.model.Item; import org.ayfaar.app.model.Link; import org.ayfaar.app.model.Term; import org.ayfaar.app.model.TermMorph; -import org.ayfaar.app.spring.Model; -import org.ayfaar.app.utils.AliasesMap; +import org.ayfaar.app.services.links.LinkService; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; import org.ayfaar.app.utils.Content; import org.ayfaar.app.utils.EmailNotifier; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.TermsMarker; +import org.hibernate.criterion.MatchMode; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.regex.Pattern.*; import static org.ayfaar.app.utils.RegExpUtils.w; import static org.ayfaar.app.utils.TermUtils.isCosmicCode; import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; -@Controller -@RequestMapping("search") +@RestController +@RequestMapping("api/search") public class SearchController { - @Autowired AliasesMap aliasesMap; + @Autowired TermService termService; @Autowired TermDao termDao; @Autowired ItemDao itemDao; + @Autowired ArticleDao articleDao; @Autowired LinkDao linkDao; @Autowired CommonDao commonDao; @Autowired TermMorphDao termMorphDao; @Autowired EmailNotifier notifier; +// @Autowired ApplicationEventPublisher eventPublisher; + @Autowired TermsMarker termsMarker; + @Autowired NewSearchController searchController; + @Autowired LinkService linkService; + @Autowired ModerationService moderationService; private Map> searchInContentCatch = new HashMap>(); @@ -50,7 +58,6 @@ private List searchInItems(String query) { }*/ @RequestMapping("content") - @Model @ResponseBody private List searchInContent(@RequestParam String query, @RequestParam(required = false, defaultValue = "0") Integer page) { @@ -72,14 +79,14 @@ private List searchInContent(@RequestParam String query, items = commonDao.findInAllContent(query, page*pageSize, pageSize); } else { - AliasesMap.Proxy proxy = aliasesMap.get(query); Term term = null; - if (proxy != null) { - term = proxy.getTerm(); + Optional providerOpt = termService.get(query); + if (providerOpt.isPresent()) { + term = providerOpt.get().getTerm(); } else { - for (Map.Entry entry : aliasesMap.entrySet()) { - if (entry.getKey().toLowerCase().equals(query)) { + for (Map.Entry entry : termService.getAll()) { + if (entry.getKey().equals(query)) { term = entry.getValue().getTerm(); break; } @@ -120,7 +127,7 @@ private List searchInContent(@RequestParam String query, query = newQuery; } - query = query.replaceAll("\\*", "[" + w + "]*"); + query = query.replaceAll("\\*", w + "*"); if (aliasesList.size() > 0) { items = commonDao.findInAllContent(aliasesList, page * pageSize, pageSize); } else { @@ -149,12 +156,11 @@ private List searchInContent(@RequestParam String query, } @RequestMapping("term") - @Model @ResponseBody - private ModelMap searchAsTerm(@RequestParam String query) { + public ModelMap searchAsTerm(@RequestParam String query) { query = query.trim(); - List allTerms = aliasesMap.getAllTerms(); - List matches = new ArrayList(); + List> allProviders = termService.getAll(); + List matches = new ArrayList<>(); Term exactMatchTerm = null; Pattern pattern = null; @@ -169,23 +175,23 @@ private ModelMap searchAsTerm(@RequestParam String query) { pattern = Pattern.compile(regexp.toLowerCase()); } - for (Term term : allTerms) { - if (term.getName().toLowerCase().equals(query.toLowerCase())) { - exactMatchTerm = term; - } else if (term.getName().toLowerCase().contains(query.toLowerCase()) - || pattern != null && pattern.matcher(term.getName().toLowerCase()).find()) { - matches.add(term.getName()); + for (Map.Entry providers : allProviders) { + if (providers.getKey().equals(query.toLowerCase())) { + exactMatchTerm = providers.getValue().getTerm(); + } else if (providers.getKey().contains(query.toLowerCase()) + || pattern != null && pattern.matcher(providers.getKey()).find()) { + matches.add(providers.getKey()); } } TermMorph morph = termMorphDao.getByName(query); if (morph != null) { - exactMatchTerm = aliasesMap.get(getValueFromUri(Term.class, morph.getTermUri())).getTerm(); + exactMatchTerm = termService.getTerm(getValueFromUri(Term.class, morph.getTermUri())); } List terms = new ArrayList(); for (String match : matches) { - Term prime = aliasesMap.get(match.toLowerCase()).getTerm(); + Term prime = termService.getTerm(match); boolean has = false; for (Term term : terms) { if (term.getUri().equals(prime.getUri())) { @@ -195,36 +201,44 @@ private ModelMap searchAsTerm(@RequestParam String query) { if (!has) terms.add(prime); } + + for (Map.Entry entry : termService.getAll()) { + String word = entry.getKey(); + pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|])|^)(" + word + + ")(([^A-Za-zА-Яа-я0-9Ёё\\]\\|])|$)", UNICODE_CHARACTER_CLASS | UNICODE_CASE | CASE_INSENSITIVE); + Matcher contentMatcher = pattern.matcher(query); + if (contentMatcher.find()) { + terms.add(entry.getValue().getTerm()); + } + } + Collections.reverse(terms); + ModelMap modelMap = new ModelMap(); modelMap.put("terms", terms); + modelMap.put("articles", articleDao.getLike("name", query, MatchMode.ANYWHERE)); modelMap.put("exactMatchTerm", exactMatchTerm); + modelMap.put("categories", searchController.inCategories(query)); return modelMap; } - @RequestMapping(value = "rate/{kind}", method = RequestMethod.POST) - public void rate(@PathVariable String kind, - @RequestParam String uri, - @RequestParam String query, - @RequestParam String quote) { - if (kind.equals("+")) { -// notifier.rate(kind, query, uri); - Term term = aliasesMap.getTerm(query); - Item item = itemDao.get(uri); - if (term != null && item != null) { - Link link = new Link(term, item, quote, "search"); + @RequestMapping(value = "rate/+", method = RequestMethod.POST) + @Moderated(value = Action.CREATE_QUOTE, command = "@searchController.addQuote") + public void addQuote(@RequestParam("query") String termName, + @RequestParam(required = false) String quote, + @RequestParam String uri) { + Term term = termService.getTerm(termName); + Item item = itemDao.get(uri); + Link link; + if (term != null && item != null) { + final List links = linkDao.get(term, item); + if (links.size() == 0) { + link = new Link(term, item, quote, termsMarker.mark(quote)); + link.setSource("search"); linkDao.save(link); + linkService.registerNew(link); + moderationService.notice(Action.QUOTE_CREATED, termName, uri); } } } - - @RequestMapping("get-content") - @ResponseBody - public String getContent(@RequestParam String uri) { - Item item = itemDao.get(uri); - if (item != null) { - return item.getContent(); - } - return null; - } } diff --git a/src/main/java/org/ayfaar/app/controllers/SongController.java b/src/main/java/org/ayfaar/app/controllers/SongController.java deleted file mode 100644 index 7da70490..00000000 --- a/src/main/java/org/ayfaar/app/controllers/SongController.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.ayfaar.app.controllers; - - -import org.ayfaar.app.dao.SongDao; -import org.ayfaar.app.model.Song; -import org.ayfaar.app.spring.Model; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -import static org.ayfaar.app.utils.ValueObjectUtils.getModelMap; - -@Controller -@RequestMapping("song") -public class SongController { - @Autowired SongDao songDao; - - @Model - @ResponseBody - @RequestMapping("{songId}") - public ModelMap get(@PathVariable Integer songId) { - Song song = songDao.getSongHtml(songId); - ModelMap modelMap = (ModelMap) getModelMap(song); - - Song next = songDao.get(song.getId() + 1); - if (next != null) { - ModelMap nextMap = new ModelMap(); - nextMap.put("uri", "песня:"+next.getId()); - nextMap.put("id", next.getId()); - modelMap.put("next", nextMap); - } - - Song prev = songDao.get(song.getId() - 1); - if (prev != null) { - ModelMap prevMap = new ModelMap(); - prevMap.put("uri", "песня:"+prev.getId()); - prevMap.put("id", prev.getId()); - modelMap.put("prev", prevMap); - } - - return modelMap; - } -} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/controllers/Suggestions.java b/src/main/java/org/ayfaar/app/controllers/Suggestions.java new file mode 100644 index 00000000..d29a018e --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/Suggestions.java @@ -0,0 +1,6 @@ +package org.ayfaar.app.controllers; + + +public enum Suggestions { + TERM,TOPIC,CATEGORY_NAME,CATEGORY_DESCRIPTION,DOCUMENT,VIDEO,VIDEO_CODE,RECORD_NAME, ITEM, IMAGES, RECORD_CODE, IMAGE_KEYWORDS +} diff --git a/src/main/java/org/ayfaar/app/controllers/SuggestionsController.java b/src/main/java/org/ayfaar/app/controllers/SuggestionsController.java index c5e3621f..c8bd7b3e 100644 --- a/src/main/java/org/ayfaar/app/controllers/SuggestionsController.java +++ b/src/main/java/org/ayfaar/app/controllers/SuggestionsController.java @@ -1,58 +1,82 @@ package org.ayfaar.app.controllers; -import org.ayfaar.app.model.Term; -import org.ayfaar.app.utils.AliasesMap; +import org.ayfaar.app.utils.TermService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.lang.Math.min; import static java.util.Arrays.asList; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.UNICODE_CASE; @Controller -@RequestMapping("v2/suggestions") +@RequestMapping("api/suggestions") public class SuggestionsController { - @Autowired AliasesMap aliasesMap; + @Autowired TermService termService; + private List escapeChars = Arrays.asList("(", ")", "[", "]", "{", "}"); public static final int MAX_SUGGESTIONS = 7; - @RequestMapping("{q}") + /** + * the syntax is {variable_name:regular_expression} + * variable named q, which value will be matched using regex .+ + */ + @RequestMapping("{q:.+}") @ResponseBody public List suggestions(@PathVariable String q) { + q = q.replace("*", ".*"); + q = q.replaceAll("\\s+", ".*"); + q = escapeRegexp(q); + q = addDuplications(q); Queue queriesQueue = new LinkedList(asList( "^"+q, "[\\s\\-]" + q, q )); - List suggestions = new ArrayList(); while (suggestions.size() < MAX_SUGGESTIONS && queriesQueue.peek() != null) { List founded = getSuggestedTerms(queriesQueue.poll(), suggestions); suggestions.addAll(founded.subList(0, min(MAX_SUGGESTIONS - suggestions.size(), founded.size()))); } + suggestions.sort((o1, o2) -> Integer.valueOf(o1.length()).compareTo(o2.length())); + return suggestions; } + protected static String addDuplications(String q) { + return q.replaceAll("([A-Za-zА-Яа-яЁё])", "$1+-*$1*"); + } + public List getSuggestedTerms(String query, List suggestions) { List terms = new ArrayList(); - Pattern pattern = Pattern.compile(query); + Pattern pattern = Pattern.compile(query,CASE_INSENSITIVE + UNICODE_CASE); - for (Term term : aliasesMap.getAllTerms()) { - Matcher matcher = pattern.matcher(term.getName().toLowerCase()); - if(matcher.find() && !suggestions.contains(term.getName())) { - terms.add(term.getName()); + for (Map.Entry entry : termService.getAll()) { + Matcher matcher = pattern.matcher(entry.getValue().getName()); + String providerName = entry.getValue().getName(); + if(matcher.find() && !suggestions.contains(providerName) && !terms.contains(providerName)) { + terms.add(entry.getValue().getName()); } } + Collections.reverse(terms); + return terms; } + + private String escapeRegexp(String query) { + for(String bracket : escapeChars) { + if(query.contains(bracket)) { + query = query.replace(bracket, "\\" + bracket); + } + } + return query; + } } diff --git a/src/main/java/org/ayfaar/app/controllers/TermController.java b/src/main/java/org/ayfaar/app/controllers/TermController.java index fbaace01..20ac1640 100644 --- a/src/main/java/org/ayfaar/app/controllers/TermController.java +++ b/src/main/java/org/ayfaar/app/controllers/TermController.java @@ -1,30 +1,35 @@ package org.ayfaar.app.controllers; -import org.apache.commons.lang.WordUtils; +import lombok.AllArgsConstructor; +import one.util.streamex.StreamEx; import org.ayfaar.app.dao.CommonDao; import org.ayfaar.app.dao.LinkDao; import org.ayfaar.app.dao.TermDao; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.TermAddEvent; import org.ayfaar.app.model.*; -import org.ayfaar.app.spring.Model; -import org.ayfaar.app.utils.AliasesMap; -import org.ayfaar.app.utils.Morpher; -import org.ayfaar.app.utils.TermUtils; -import org.ayfaar.app.utils.ValueObjectUtils; +import org.ayfaar.app.services.EntityLoader; +import org.ayfaar.app.services.itemRange.ItemRangeService; +import org.ayfaar.app.services.links.LinkService; +import org.ayfaar.app.services.topics.TopicProvider; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.utils.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; +import javax.inject.Inject; import java.util.*; +import java.util.function.Supplier; -import static java.util.Collections.sort; -import static org.ayfaar.app.model.Link.*; +import static org.ayfaar.app.model.LinkType.ABBREVIATION; +import static org.ayfaar.app.model.LinkType.ALIAS; import static org.ayfaar.app.utils.ValueObjectUtils.convertToPlainObjects; -import static org.ayfaar.app.utils.ValueObjectUtils.getModelMap; -import static org.springframework.util.Assert.notNull; +import static org.springframework.util.StringUtils.isEmpty; @Controller -@RequestMapping("term") +@RequestMapping("api/term") public class TermController { private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(TermController.class.getName()); @@ -32,74 +37,68 @@ public class TermController { @Autowired CommonDao commonDao; @Autowired TermDao termDao; @Autowired LinkDao linkDao; - @Autowired AliasesMap aliasesMap; - @Autowired - SuggestionsController searchController2; - - /*@RequestMapping("import") - @Model - public void _import() throws ParserConfigurationException, SAXException, IOException { - for (Termin termin : commonDao.getAll(Termin.class)) { - add(termin.getName()); - } - }*/ + @Autowired TermService termService; + @Autowired LinkService linkService; + @Autowired EntityLoader entityLoader; + @Autowired TermServiceImpl aliasesMap; + @Autowired SuggestionsController suggestionsController; + @Inject TermsMarker termsMarker; + @Inject EventPublisher publisher; + @Inject NewSearchController searchController; + @Inject ItemRangeService itemRangeService; + @Inject TermsFinder termsFinder; + @Inject TopicService topicService; + @RequestMapping(value = "add", method = RequestMethod.POST) - @Model - public void add(Term term) { - add(term.getName(), term.getShortDescription(), term.getDescription()); + @ResponseBody + public Term add(Term term) { + return add(term.getName(), term.getShortDescription(), term.getDescription()); } @RequestMapping(value = "/", method = RequestMethod.GET) - @Model - public ModelMap get(@RequestParam("name") String termName) { - Term term = termDao.getByName(termName); - notNull(term, "Термин не найден"); - return get(term); - } - - public ModelMap get(Term term) { - String termName = term.getName(); - Term alias = null; - if (!aliasesMap.get(termName).getTerm().getUri().equals(term.getUri())) { - alias = term; - term = aliasesMap.get(termName).getTerm(); + @ResponseBody + public ModelMap get(@RequestParam("name") String termName, @RequestParam(required = false) boolean mark) { + termName = termName.replace("_", " "); + Optional providerOpt = termService.getMainOrThis(termName); + if (!providerOpt.isPresent()) { + return null; } + final TermService.TermProvider provider = providerOpt.get(); + + ModelMap modelMap = new ModelMap();//(ModelMap) getModelMap(term); + Term term = provider.getTerm(); - // может быть аббравиатурой, сокращением, кодом или синонимов - Link _link = linkDao.getForAbbreviationOrAliasOrCode(term.getUri()); - if (_link != null && _link.getUid1() instanceof Term) { - alias = term; - term = (Term) _link.getUid1(); + modelMap.put("uri", term.getUri()); + modelMap.put("name", term.getName()); + if (mark) { + if (isEmpty(term.getTaggedShortDescription()) && !isEmpty(term.getShortDescription())) { + term.setTaggedShortDescription(termsMarker.mark(term.getShortDescription())); + termDao.save(term); + } + if (isEmpty(term.getTaggedDescription()) && !isEmpty(term.getDescription())) { + term.setTaggedDescription(termsMarker.mark(term.getDescription())); + termDao.save(term); + } + modelMap.put("shortDescription", term.getTaggedShortDescription()); + modelMap.put("description", term.getTaggedDescription()); + } else { + modelMap.put("shortDescription", term.getShortDescription()); + modelMap.put("description", term.getDescription()); } -// } - ModelMap modelMap = (ModelMap) getModelMap(term); + // LINKS - modelMap.put("from", alias); + List quotes = new ArrayList<>(); + linkService.getAllLinksBetween(provider.getUri(), Item.class) + .forEach(p -> quotes.add(getQuote(p.taggedQuote(), p.get(Item.class).get()))); - // LINKS + Set related = new LinkedHashSet<>(); + Set aliases = new LinkedHashSet<>(); - List quotes = new ArrayList(); - Set related = new LinkedHashSet(); - Set aliases = new LinkedHashSet(); - - UID code = null; - List links = linkDao.getAllLinks(term.getUri()); - for (Link link : links) { - UID source = link.getUid1().getUri().equals(term.getUri()) - ? link.getUid2() - : link.getUid1(); - if (link.getQuote() != null || source instanceof Item) { - quotes.add(getQuote(link, source)); - } else if (ABBREVIATION.equals(link.getType()) || ALIAS.equals(link.getType())) { - aliases.add(source); - } else if (CODE.equals(link.getType())) { - code = source; - } else { - related.add(source); - } - } + provider.getAbbreviations().forEach(p -> aliases.add(p.getTerm())); + provider.getAliases().forEach(p -> aliases.add(p.getTerm())); + UID code = provider.getCode().isPresent() ? provider.getCode().get().getTerm() : null; // Нужно также включить цитаты всех синонимов и сокращений и кода Set aliasesQuoteSources = new HashSet(aliases); @@ -107,57 +106,89 @@ public ModelMap get(Term term) { aliasesQuoteSources.add(code); } for (UID uid : aliasesQuoteSources) { - List aliasLinksWithQuote = linkDao.getAllLinks(uid.getUri()); - for (Link link : aliasLinksWithQuote) { - UID source = link.getUid1().getUri().equals(uid.getUri()) - ? link.getUid2() - : link.getUid1(); - if (link.getQuote() != null || source instanceof Item) { - quotes.add(getQuote(link, source)); - } else if (ABBREVIATION.equals(link.getType()) || ALIAS.equals(link.getType()) || CODE.equals(link.getType())) { - // Синонимы синонимов :) по идее их не должно быть, но если вдруг... - // как минимум один есть и этот наш основной термин - if (!source.getUri().equals(term.getUri())) { - aliases.add(source); - } - } else { - related.add(source); - } - } + linkService.getAllLinksFor(uid.getUri()) + .forEach(p -> { + final UID source = entityLoader.get(p.not(uid.getUri())); + if (source instanceof Item) { + final Quote quote = getQuote(p.taggedQuote(), source.getUri()); + if (quotes.stream().noneMatch(q -> Objects.equals(quote.quote, q.quote))) { + quotes.add(quote); + } + } + else if (ABBREVIATION.equals(p.type()) || ALIAS.equals(p.type())/* || CODE.equals(p.type())*/) { + aliases.add(source); + } + else { + related.add(source); + } + }); } - sort(quotes, new Comparator() { - @Override - public int compare(ModelMap o1, ModelMap o2) { - return ((String) o1.get("uri")).compareTo((String) o2.get("uri")); - } - }); + + aliases.removeIf(item -> item.getUri().equals(term.getUri())); + + quotes.sort(Comparator.comparing(o -> o.uri)); + + // mark quotes with strong + List allAliasesWithAllMorphs = provider.getAllAliasesWithAllMorphs(); + for (Quote quote : quotes) { + String text = quote.quote; + if (text == null || text.isEmpty() || text.contains("strong")) continue; + quote.quote = StringUtils.markWithStrong(text, allAliasesWithAllMorphs); + } + + Optional topicOpt = StreamEx.>>of( + () -> topicService.findByName(term.getName()), // получаем топик по имени термина + () -> topicService.contains(term.getName()), + () -> topicService.getAllLinkedWith(term.getUri()).findFirst(), // по линку с термином + () -> allAliasesWithAllMorphs.stream().map(a -> topicService.findByName(a)).filter(Optional::isPresent).findFirst().orElseGet(Optional::empty), // по имени по всем алиасам + () -> allAliasesWithAllMorphs.stream().map(a -> topicService.getAllLinkedWith(UriGenerator.generate(Term.class, a)).findFirst()).filter(Optional::isPresent).findFirst().orElseGet(Optional::empty)) // по линками по всем алиасам + + .map(Supplier::get) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); modelMap.put("code", code); modelMap.put("quotes", quotes); modelMap.put("related", toPlainObjectWithoutContent(related)); modelMap.put("aliases", toPlainObjectWithoutContent(aliases)); + modelMap.put("categories", searchController.inCategories(termName)); + topicOpt.ifPresent(topicProvider -> { + modelMap.put("topicResources", topicProvider.resources()); + }); return modelMap; } private Object toPlainObjectWithoutContent(Set related) { - return convertToPlainObjects(related, new ValueObjectUtils.Modifier() { - @Override - public void modify(UID entity, ModelMap map) { - map.remove("content"); - } - }); + return convertToPlainObjects(related, (entity, map) -> map.remove("content")); } - private ModelMap getQuote(Link link, UID source) { + private ModelMap getQuote(Link link, UID source, boolean mark) { ModelMap map = new ModelMap(); - map.put("quote", link.getQuote() != null ? link.getQuote() : ((Item) source).getContent()); + String quote = link.getQuote() != null ? link.getQuote() : ((Item) source).getContent(); + if (mark) { + quote = link.getTaggedQuote() != null ? link.getTaggedQuote() : ((Item) source).getTaggedContent(); + } + map.put("quote", quote); map.put("uri", source.getUri()); return map; } + private Quote getQuote(String taggedQuote, String itemUri) { + if (isEmpty(taggedQuote)) { + taggedQuote = entityLoader.get(itemUri).getTaggedContent(); + } + return new Quote(taggedQuote, itemUri); + } + + @AllArgsConstructor + class Quote { + public String quote; + public String uri; + } + @RequestMapping("related") - @Model @ResponseBody public Collection getRelated(@RequestParam String uri) { Set related = new LinkedHashSet(); @@ -170,12 +201,7 @@ public Collection getRelated(@RequestParam String uri) { } } } - return convertToPlainObjects(related, new ValueObjectUtils.Modifier() { - @Override - public void modify(UID entity, ModelMap map) { - map.remove("content"); - } - }); + return convertToPlainObjects(related, (entity, map) -> map.remove("content")); } // @RequestMapping(value = "/", method = POST) @@ -186,12 +212,16 @@ public Term add(String name, String description) { public Term add(String name, String shortDescription, String description) { name = name.replace("\"", "").replace("«", "").replace("»", "").trim(); - name = WordUtils.capitalize(name, new char[]{'@'}); // Делаем первую букву большой, @ - знак который не появляеться в названии, чтобы поднялась только первая буква всей фразы +// name = WordUtils.capitalize(name, new char[]{'@'}); // Делаем первую букву большой, @ - знак который не появляеться в названии, чтобы поднялась только первая буква всей фразы Term term = termDao.getByName(name); if (term == null) { term = termDao.save(new Term(name, shortDescription, description)); + if (shortDescription != null) + term.setTaggedShortDescription(termsMarker.mark(shortDescription)); + if (description != null) + term.setTaggedDescription(termsMarker.mark(description)); String termName = term.getName(); - log.info("Added: "+ termName); + log.info("Added: " + termName); if (TermUtils.isComposite(termName)) { String target = TermUtils.getNonCosmicCodePart(termName); if (target != null) { @@ -200,18 +230,25 @@ public Term add(String name, String shortDescription, String description) { } else if (!TermUtils.isCosmicCode(termName)) { findAliases(term, termName, ""); } +// publisher.publishEvent(new NewTermEvent(term)); } else { - term.setShortDescription(shortDescription); - term.setDescription(description); + String oldShortDescription = null; + if (shortDescription != null && !shortDescription.isEmpty()) { + oldShortDescription = term.getShortDescription(); + term.setShortDescription(shortDescription); + term.setTaggedShortDescription(termsMarker.mark(shortDescription)); + } + String oldDescription = null; + if (description != null && !description.isEmpty()) { + oldDescription = term.getDescription(); + term.setDescription(description); + term.setTaggedDescription(termsMarker.mark(description)); + } termDao.save(term); +// publisher.publishEvent(new TermUpdatedEvent(term, oldShortDescription, oldDescription)); } - new Thread(new Runnable() { - @Override - public void run() { - aliasesMap.reload(); - } - }).start(); + publisher.publishEvent(new TermAddEvent(term.getName())); return term; } @@ -229,12 +266,11 @@ private void findAliases(Term primeTerm, String target, String prefix) { String alias = prefix+morph.text; if (morph != null && !alias.isEmpty() && !alias.equals(primeTerm.getName())) { - if (!aliases.contains(alias)) { TermMorph termMorph = commonDao.get(TermMorph.class, alias); if (termMorph == null) { commonDao.save(new TermMorph(alias, primeTerm.getUri())); - aliasesMap.put(alias, primeTerm); + log.info("Alias added: "+alias); } aliases.add(alias); @@ -242,7 +278,7 @@ private void findAliases(Term primeTerm, String target, String prefix) { } } /* for (Map.Entry entry : aliases.entrySet()) { - if (primeTerm.getUri().equals(entry.getValue().generateUri())) continue; + if (primeTerm.uri().equals(entry.getValue().generateUri())) continue; commonDao.save(new Link(primeTerm, entry.getValue(), Link.ALIAS, Link.MORPHEME_WEIGHT)); }*/ } @@ -252,8 +288,6 @@ private void findAliases(Term primeTerm, String target, String prefix) { } } - - public Term getPrime(Term term) { return (Term) linkDao.getPrimeForAlias(term.getUri()); } @@ -265,7 +299,13 @@ public Term add(String termName) { @RequestMapping("autocomplete") @ResponseBody public List autoComplete(@RequestParam("filter[filters][0][value]") String filter) { - return searchController2.suggestions(filter); + return suggestionsController.suggestions(filter); + } + + @RequestMapping(value = "get-short-description", produces = "text/plain; charset=utf-8") + @ResponseBody + public String getShortDescription(@RequestParam String name) { + return termService.getTerm(name).getShortDescription(); } @RequestMapping("remove/{name}") @@ -276,6 +316,6 @@ public void remove(@PathVariable String name) { @RequestMapping("reload-aliases-map") public void reloadAliasesMap() { - aliasesMap.reload(); + termService.reload(); } } diff --git a/src/main/java/org/ayfaar/app/controllers/TopicController.java b/src/main/java/org/ayfaar/app/controllers/TopicController.java new file mode 100644 index 00000000..13cf6d12 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/TopicController.java @@ -0,0 +1,291 @@ +package org.ayfaar.app.controllers; + +import lombok.Builder; +import org.ayfaar.app.annotations.Moderated; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.dao.LinkDao; +import org.ayfaar.app.model.ItemsRange; +import org.ayfaar.app.model.Link; +import org.ayfaar.app.model.Topic; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.services.links.LinkService; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.services.topics.TopicProvider; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.translation.TopicTranslationSynchronizer; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.UriGenerator; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.*; + +import static java.util.stream.Collectors.toList; +import static org.springframework.util.Assert.hasLength; +import static org.springframework.util.StringUtils.isEmpty; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@RestController +@RequestMapping("api/topic") +public class TopicController { + + final CommonDao commonDao; + private final LinkDao linkDao; + private final TopicService topicService; + private final ModerationService moderationService; + private final LinkService linkService; + private final NewSuggestionsController suggestionsController; + private TopicTranslationSynchronizer translationSynchronizer; + + @Inject + public TopicController(TopicService topicService, NewSuggestionsController suggestionsController, ModerationService moderationService, LinkService linkService, CommonDao commonDao, LinkDao linkDao, TopicTranslationSynchronizer translationSynchronizer) { + this.topicService = topicService; + this.suggestionsController = suggestionsController; + this.moderationService = moderationService; + this.linkService = linkService; + this.commonDao = commonDao; + this.linkDao = linkDao; + this.translationSynchronizer = translationSynchronizer; + } + + @RequestMapping("for/{uri}") + /** + * Get all topics with any UID by his uri + */ + public List getForUri(@PathVariable String uri) throws Exception { + // todo: move to TopicService and sort by link rate + hasLength(uri); + final List links = linkDao.getAllLinks(uri); + List presentations = new ArrayList<>(); + for (Link link : links) { + Topic topic = null; + if (link.getUid1() instanceof Topic) { + topic = (Topic) link.getUid1(); + } + if (link.getUid2() instanceof Topic) { + topic = (Topic) link.getUid2(); + } + if (topic != null) + presentations.add(new LinkedTopicPresentation(topic, link.getRate(), link.getComment())); + } + Collections.sort(presentations, (o1, o2) -> o1.rate == null || o2.rate == null ? 0 : -o1.rate.compareTo(o2.rate)); + return presentations; + } + + private class LinkedTopicPresentation { + public String comment; + public String uri; + public String name; + public Float rate; + + LinkedTopicPresentation(Topic topic, Float rate, String comment) { + this.comment = comment; + name = topic.getName(); + uri = topic.getUri(); + this.rate = rate; + } + } + + @RequestMapping(value = "import", method = POST) + @Moderated(value = Action.TOPIC_CREATE, command = "@topicController.importTopics") + public void importTopics(@RequestBody String topics) throws Exception { + hasLength(topics); + final Set uniqueNames = new HashSet<>(Arrays.asList(topics.split("\n"))); + uniqueNames.stream() + .filter(name -> !name.isEmpty()) + .forEachOrdered(name -> commonDao.save(new Topic(name))); + topicService.reload(); + } + + @RequestMapping(value = "for", method = POST) + @Moderated(value = Action.TOPIC_LINK_RESOURCE, command = "@topicController.addFor") + public Topic addFor(@RequestParam String uri, + @RequestParam String name, + @RequestParam(required = false) String quote, + @RequestParam(required = false) String comment, + @RequestParam(required = false) Float rate) throws Exception { + UID uid = commonDao.get(UriGenerator.getClassByUri(uri), uri); + final TopicProvider topic = topicService.findOrCreate(name); + topic.link(null, uid, comment, quote, rate); + moderationService.notice(Action.TOPIC_RESOURCE_LINKED, topic.name(), uid.getUri()); + return topic.topic(); + } + + @RequestMapping(value = "for/items-range", method = POST) + @Moderated(value = Action.TOPIC_LINK_RANGE, command = "@topicController.addFor") + public void addFor(@RequestParam String from, + @RequestParam String to, + @RequestParam String topicName, + @RequestParam(required = false) String rangeName, + @RequestParam(required = false) String quote, + @RequestParam(required = false) String comment, + @RequestParam(required = false) Float rate) throws Exception { + ItemsRange itemsRange = ItemsRange.builder().from(from).to(to).description(rangeName).build(); + final String itemsRangeUri = UriGenerator.generate(itemsRange); + ItemsRange range = commonDao.get(ItemsRange.class, itemsRangeUri); + if (range == null) { + moderationService.check(Action.ITEMS_RANGE_CREATE); + range = commonDao.save(itemsRange); + } else { + moderationService.check(Action.ITEMS_RANGE_UPDATE); + range.setDescription(rangeName); + } + + final TopicProvider topic = topicService.findOrCreate(topicName); + topic.link(null, range, comment, quote, rate); + } + + @RequestMapping(value = "update-rate", method = POST) + @Moderated(value = Action.TOPIC_RESOURCE_LINK_RATE_UPDATE, command = "@topicController.updateRate") + public void updateRate(@RequestParam String forUri, @RequestParam String name, @RequestParam Float rate) throws Exception { + // fixme: для свеже созданой связи между видео и темой не возможно изменить рейт + linkService.getByUris(forUri, UriGenerator.generate(Topic.class, name)).get().updater().rate(rate).commit(); + } + + @RequestMapping(value = "update-comment", method = POST) + @Moderated(value = Action.TOPIC_RESOURCE_LINK_COMMENT_UPDATE, command = "@topicController.updateComment") + public void updateComment(@RequestParam String forUri, @RequestParam String name, @RequestParam String comment) throws Exception { + linkService.getByUris(forUri, UriGenerator.generate(Topic.class, name)).get().updater().comment(comment).commit(); + } + + @RequestMapping(value = "unlink-uri", method = POST) + @Moderated(value = Action.TOPIC_UNLINK_RESOURCE, command = "@topicController.unlinkUri") + public void unlinkUri(@RequestParam String uri, @RequestParam String topicUri) throws Exception { + hasLength(uri); + hasLength(topicUri); + // todo: move this logic to topic service + final List links = linkDao.getAllLinks(uri); + for (Link link : links) { + if ((link.getUid1() instanceof Topic && link.getUid1().getUri().equals(topicUri)) || + (link.getUid2() instanceof Topic && link.getUid2().getUri().equals(topicUri))) { + linkDao.remove(link.getLinkId()); + moderationService.notice(Action.TOPIC_RESOURCE_UNLINKED, topicUri, uri); + return; // remove only first one + } + } + } + + @RequestMapping("suggest") + public Collection suggest(@RequestParam(name = "q", required = false) String q, + @RequestParam(name = "filter[filters][0][value]", required = false) String kendoQ) { + if (kendoQ != null) q = kendoQ; + return suggestionsController.suggestions(q, false, true, false, false, false, false, false, false, false, false, false).values(); + } + + @RequestMapping("add-child") + @Moderated(value = Action.TOPIC_ADD_CHILD, command = "@topicController.addChild") + public void addChild(@RequestParam String child, @RequestParam String name) { + if (topicService.exist(name) && topicService.exist(child)) { + boolean alreadyParent = topicService.getByName(child).children().anyMatch(c -> c.name().equals(name)); + if (alreadyParent) { + throw new RuntimeException("The parent has a child for the given name"); + } + } + final TopicProvider parentTopic = topicService.findOrCreate(name); + if (!parentTopic.getChild(child).isPresent()) { + final TopicProvider childTopic = parentTopic.addChild(child); + moderationService.notice(Action.TOPIC_CHILD_ADDED, parentTopic.name(), childTopic.name()); + } + } + + @RequestMapping(value = "unlink", method = POST) + @Moderated(value = Action.TOPIC_UNLINK_TOPIC, command = "@topicController.unlink") + public void unlink(@RequestParam String name, @RequestParam String linked) { + final TopicProvider unlinked = topicService.getByName(name).unlink(linked); + if (unlinked != null) moderationService.notice(Action.TOPIC_TOPIC_UNLINKED, name, linked); + } + + @RequestMapping("merge") + // Слияние двух веток + @Moderated(value = Action.TOPIC_MERGE, command = "@topicController.merge") + public void merge(@RequestParam String main, @RequestParam String mergeInto) { + topicService.getByName(main, true).merge(mergeInto); + } + + /*@RequestMapping("add-related") + public void addRelated(@RequestParam String name, @RequestParam String related) { + topicService.findOrCreate(name).link(topicService.findOrCreate(related).topic()); + }*/ + + @RequestMapping("children") + public List linkChild(@RequestParam String name) { + return topicService.getByName(name) + .children() + .map(TopicProvider::topic).collect(toList()); + } + + @RequestMapping("parents") + public List linkParent(@RequestParam String name) { + return topicService.getByName(name) + .parents() + .map(TopicProvider::topic).collect(toList()); + } + + + @RequestMapping + public GetTopicPresentation get(@RequestParam String name, @RequestParam(required = false) boolean includeResources) { + TopicProvider topic = topicService.getByName(name); + String linkedTerm = topic.linkedTerm().map(TermService.TermProvider::getName).orElse(null); + String suggestedTerm = isEmpty(linkedTerm) ? topic.relatedTermSuggestion().map(TermService.TermProvider::getName).orElse(null) : null; + return GetTopicPresentation.builder() + .name(topic.name()) + .uri(topic.uri()) + .children(topic.children().map(TopicProvider::name).collect(toList())) + .parents(topic.parents().map(TopicProvider::name).collect(toList())) + .related(topic.related().map(TopicProvider::name).collect(toList())) + .resources(includeResources ? topic.resources() : null) + .term(linkedTerm) + .suggestedTerm(suggestedTerm) + .build(); + } + + @Builder + private static class GetTopicPresentation { + public String uri; + public String name; + public String term; + public String suggestedTerm; + public List children; + public List parents; + public List related; + public TopicProvider.TopicResources resources; + } + + @RequestMapping("reload") + public void reload() { + topicService.reload(); + } + + @RequestMapping("bulk/link") + //todo implement + public void bulkLinkResources(String topicName, List resourceUris) { + // link each resource to topicName + throw new RuntimeException("Unimplemented"); + } + + @RequestMapping("bulk/unlink") + //todo implement + public void bulkUnlinkResources(String topicName, List resourceUris) { + // unlink each resource from topicName + throw new RuntimeException("Unimplemented"); + } + + @RequestMapping("last") + public List getLast(@PageableDefault @SortDefault(direction = Sort.Direction.DESC, sort = "createdAt") Pageable pageable) { + return commonDao.getPage(Topic.class, pageable); + } + + @RequestMapping(value = "sync-translation", method = GET) + public ResponseEntity syncTranslation() { + translationSynchronizer.synchronize(); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/UserController.java b/src/main/java/org/ayfaar/app/controllers/UserController.java new file mode 100644 index 00000000..42e433f9 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/UserController.java @@ -0,0 +1,68 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.services.moderation.UserRole; +import org.ayfaar.app.utils.exceptions.ExceptionCode; +import org.ayfaar.app.utils.exceptions.LogicalException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import javax.inject.Inject; +import java.util.List; + + +@RestController +@RequestMapping("api/user") +public class UserController { + @Inject CommonDao commonDao; + @Inject ModerationService moderationService; + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping + public List getAll(@PageableDefault Pageable pageable) { + return commonDao.getPage(User.class, pageable); + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("{email}") + public User getUserDetail(@PathVariable String email) { + return commonDao.getOpt(User.class, email).orElseThrow(() -> new LogicalException(ExceptionCode.USER_NOT_FOUND, email)); + } + + @Secured("hasRole('ROLE_ADMIN')") + @RequestMapping(value = "update-role", method = RequestMethod.POST) + public void updateRole(@RequestParam String email, @RequestParam int numRole){ + User user = commonDao.getOpt(User.class, email).orElseThrow(() -> new LogicalException(ExceptionCode.USER_NOT_FOUND, email)); + final UserRole accessLevel = UserRole.fromPrecedence(numRole) + .orElseThrow(() -> new LogicalException(ExceptionCode.ROLE_NOT_FOUND, numRole)); + user.setRole(accessLevel); + commonDao.save(user); + } + + @RequestMapping("current") + public User getCurrent(@AuthenticationPrincipal User current){ + return current; + } + + @RequestMapping(value = "current/rename", method = RequestMethod.POST) + @Secured("authenticated") + public User renameCurrent(@AuthenticationPrincipal User current, @RequestParam String name){ + final String oldName = current.getName(); + current.setName(name); + commonDao.save(current); + moderationService.notice(Action.USER_RENAME, oldName, name); + return current; + } + + @RequestMapping(value = "hide-actions-before/{id}", method = RequestMethod.POST) + public void hideActions(@AuthenticationPrincipal User user, @PathVariable Integer id) { + user.setHiddenActionEventId(id); + commonDao.save(user); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/VideoResourcesController.java b/src/main/java/org/ayfaar/app/controllers/VideoResourcesController.java new file mode 100644 index 00000000..9f0fb64e --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/VideoResourcesController.java @@ -0,0 +1,110 @@ +package org.ayfaar.app.controllers; + +import org.ayfaar.app.annotations.Moderated; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.User; +import org.ayfaar.app.model.VideoResource; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.utils.GoogleService; +import org.ayfaar.app.utils.Language; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.List; + +import static org.ayfaar.app.utils.GoogleService.extractVideoIdFromYoutubeUrl; +import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.util.Assert.hasLength; +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@RestController +@RequestMapping({"api/resource/video", "api/video"}) +public class VideoResourcesController { + + private final CommonDao commonDao; + private final GoogleService youtubeService; + private final ModerationService moderationService; + + @Inject + public VideoResourcesController(GoogleService youtubeService, CommonDao commonDao, ModerationService moderationService) { + this.youtubeService = youtubeService; + this.commonDao = commonDao; + this.moderationService = moderationService; + } + + @RequestMapping("{id}") + public VideoResource get(@PathVariable String id) throws Exception { + VideoResource video = commonDao.get(VideoResource.class, "id", id); + if (video != null) { + if (video.getTitle() == null) { + final GoogleService.VideoInfo info = youtubeService.getVideoInfo(id); + video.setTitle(info.title); + video.setPublishedAt(info.publishedAt); + commonDao.save(video); + } + return video; + } + return null;//commonDao.save(new VideoResource(id, Language.ru)); + } + + @RequestMapping("last-created") + public List lastCreated(@PageableDefault(size = 6, sort = "createdAt", direction = DESC) Pageable pageable) throws Exception { + return commonDao.getPage(VideoResource.class, pageable); + } + + @RequestMapping(method = POST) + @Moderated(value = Action.VIDEO_ADD, command = "@videoResourcesController.add") + public VideoResource add(@RequestParam String url, @AuthenticationPrincipal User user) throws Exception { + hasLength(url); + final String videoId = extractVideoIdFromYoutubeUrl(url); + return commonDao.getOpt(VideoResource.class, "id", videoId).orElseGet(() -> { + final GoogleService.VideoInfo info = youtubeService.getVideoInfo(videoId); + final VideoResource video = new VideoResource(videoId, Language.ru); + video.setTitle(info.title); + video.setPublishedAt(info.publishedAt); + + youtubeService.getCodeVideoFromYoutube(videoId).ifPresent(video::setCode); + + if (user != null) video.setCreatedBy(user.getId()); + commonDao.save(video); + moderationService.notice(Action.VIDEO_ADDED, video.getTitle(), video.getUri()); + return video; + }); + } + + @RequestMapping(value = "update-title", method = RequestMethod.POST) + @Moderated(value = Action.VIDEO_UPDATE_TITLE, command = "@videoResourcesController.updateTitle") + public void updateTitle(@RequestParam String uri, @RequestParam String title) { + hasLength(uri); + VideoResource video = commonDao.get(VideoResource.class, "uri", uri); + if (video != null) { + video.setTitle(title); + commonDao.save(video); + } + } + + @RequestMapping("{id}/remove") + @Moderated(value = Action.VIDEO_REMOVE, command = "@videoResourcesController.remove") + public void remove(@PathVariable String id) { + commonDao.getOpt(VideoResource.class, "id", id).ifPresent(video -> { + commonDao.remove(video); + moderationService.notice(Action.VIDEO_REMOVED, video.getTitle(), video.getId()); + // todo: update linked entities (topic links for example) + }); + } + + @RequestMapping(value = "update-code", method = POST) + @Moderated(value = Action.VIDEO_UPDATE_CODE, command = "@videoResourcesController.updateCode") + public void updateCode(@RequestParam String id, @RequestParam String code) { + commonDao.getOpt(VideoResource.class, "id", id).ifPresent(video -> { + final String oldCode = video.getCode(); + video.setCode(code); + commonDao.save(video); + moderationService.notice(Action.VIDEO_CODE_UPDATED, video.getTitle(), video.getUri(), oldCode != null ? oldCode : "<пусто>", code); + }); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/Cache.java b/src/main/java/org/ayfaar/app/controllers/search/Cache.java deleted file mode 100644 index c045e56c..00000000 --- a/src/main/java/org/ayfaar/app/controllers/search/Cache.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.ayfaar.app.controllers.search; - - -import lombok.Data; -import org.ayfaar.app.controllers.NewSearchController; - -import java.util.List; - -//@Data -public class Cache { - private List quotes; - - public List getCache(String query, Integer pageNumber, String filter) { - int start = pageNumber * NewSearchController.PAGE_SIZE; - int temp = start + NewSearchController.PAGE_SIZE; - int end = temp < quotes.size() ? temp : quotes.size(); - return quotes.subList(start, end); - } -} diff --git a/src/main/java/org/ayfaar/app/controllers/search/Quote.java b/src/main/java/org/ayfaar/app/controllers/search/Quote.java index 7b844d22..d263c7c6 100644 --- a/src/main/java/org/ayfaar/app/controllers/search/Quote.java +++ b/src/main/java/org/ayfaar/app/controllers/search/Quote.java @@ -4,6 +4,7 @@ @Data public class Quote { - private String uri; // уникальный идентификатор источника +// private String uri; // уникальный идентификатор источника + private String number; private String quote; } diff --git a/src/main/java/org/ayfaar/app/controllers/search/SearchCache.java b/src/main/java/org/ayfaar/app/controllers/search/SearchCache.java new file mode 100644 index 00000000..3b6b2f20 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/SearchCache.java @@ -0,0 +1,40 @@ +package org.ayfaar.app.controllers.search; + +public interface SearchCache { + /** + * Генерация уникального ключа с зависимостью от заданных аргументов. + * То есть для одинаковых аргументов генерируется одинаковые ключи + * + * @param query + * @param pageNumber + * @param fromItemNumber + * @return уникальный ключ + */ + Object generateKey(String query, Integer pageNumber, String fromItemNumber); + + /** + * Сообщает о том есть ли сохранённый данные для указаного ключа + * + * @param cacheKey результат generateKey + * @return true если есть + */ + boolean has(Object cacheKey); + + /** + * Возвращает сохранённое ранее значение по ключу + * + * @param cacheKey результат generateKey + * @return страница результата поиска + */ + SearchResultPage get(Object cacheKey); + + /** + * Сохранение в кеше результат поиска + * + * @param cacheKey результат generateKey + * @param page страница поиска + */ + void put(Object cacheKey, SearchResultPage page); + + void clean(); +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/SearchCacheImpl.java b/src/main/java/org/ayfaar/app/controllers/search/SearchCacheImpl.java new file mode 100644 index 00000000..b09b4e5b --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/SearchCacheImpl.java @@ -0,0 +1,46 @@ +package org.ayfaar.app.controllers.search; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class SearchCacheImpl implements SearchCache { + private Map cache; + + public SearchCacheImpl() { + cache = new HashMap(); + } + + @Override + public Object generateKey(String query, Integer pageNumber, String fromItemNumber) throws NullPointerException{ + if (fromItemNumber != null) { + return query + pageNumber + fromItemNumber; + } else { + return query + pageNumber + "null"; + } + } + + @Override + public boolean has(Object cacheKey) { + return cache.containsKey(cacheKey) && cache.get(cacheKey) != null; + } + + @Override + public SearchResultPage get(Object cacheKey){ + return cache.get(cacheKey); + } + + @Override + public void put(Object cacheKey, SearchResultPage page){ + if (cacheKey == null) throw new IllegalArgumentException(); + if (page == null) throw new IllegalArgumentException(); + cache.put(cacheKey, page); + } + + @Override + public void clean() { + cache.clear(); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/SearchQuotesHelper.java b/src/main/java/org/ayfaar/app/controllers/search/SearchQuotesHelper.java index d2597f62..8ef303fe 100644 --- a/src/main/java/org/ayfaar/app/controllers/search/SearchQuotesHelper.java +++ b/src/main/java/org/ayfaar/app/controllers/search/SearchQuotesHelper.java @@ -1,22 +1,168 @@ package org.ayfaar.app.controllers.search; -import org.apache.commons.lang.NotImplementedException; import org.ayfaar.app.model.Item; +import org.ayfaar.app.utils.RegExpUtils; +import org.ayfaar.app.utils.TermsMarker; import org.springframework.stereotype.Component; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.commons.lang3.StringUtils.join; -//5.Обработка найденных пунктов @Component public class SearchQuotesHelper { + protected static int MAX_WORDS_ON_BOUNDARIES = 50; + private static final String forCreateLeftPartQuote = "([^\\.\\?!]*)([\\.\\?!]*)(\\.|\\?|\\!)(\\)|\\»| -| –)"; + private static final String forCreateRightPartQuote = "(\\)|\\»| -| –)[^\\.\\?!]*[\\.\\?!]*"; + private static final List brackets = Arrays.asList(".)", "!)", "?)", ".»", "!»", "?»"); + private static final List directSpeech = Arrays.asList(". –", "! –", "? –", ". -", "! -", "? -"); + private static final List openQuoteBracketHyphen = Arrays.asList('«', '(', '-'); + private static final List punctual = Arrays.asList(".", "?", "!", ":", ";"); - public static final int MAX_WORDS_ON_BOUNDARIES = 30; + @Inject TermsMarker termsMarker; public List createQuotes(List foundedItems, List allPossibleSearchQueries) { - // пройтись по всем пунктам и вырезать предложением, в котором встречаеться поисковая фраза или фразы - // Если до или после найденной фразы слов больше чем 30 (MAX_WORDS_ON_BOUNDARIES), - // то обрезать всё до (или после) 30 слова и поставить "..." - // Обозначить поисковую фразу или фразы тегами фраза - throw new NotImplementedException(); + List quotes = new ArrayList(); + String forLeftPart = "([\\.\\?!]*)([^\\.\\?!]*)()"; + String forRightPart = "[^\\.\\?!]+[\\.\\?!]*[^\\.\\?!]*[\\.\\?!]*"; + String regexp = join(allPossibleSearchQueries, "|"); + regexp = regexp.replace("*", RegExpUtils.w+"*"); + + + for (Item item : foundedItems) { + String content = ""; + Pattern pattern = Pattern.compile("(^" + regexp + ")|(" + RegExpUtils.W + "+" + regexp + RegExpUtils.W + + "+)|(" + regexp + "$)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + Matcher matcher = pattern.matcher(item.getTaggedContent()); + + if (matcher.find()) { + if(punctual.contains(regexp.substring(regexp.length()-1))) { + String lastCharacter = regexp.substring(regexp.length()-1); + String newRegexp = lastCharacter.equals(".") || lastCharacter.equals("?") ? + regexp.substring(0, regexp.length()-1) + "\\" + lastCharacter : regexp; + content = item.getTaggedContent().replaceAll("(?iu)(" + newRegexp + ")", "$1"); + } + else { + content = item.getTaggedContent().replaceAll("(?iu)\\b(" + regexp + ")\\b", "$1"); + } + } + + String[] phrases = content.split(""); + String leftPart = getPartQuote(phrases[0] + "", forLeftPart, "", "left"); + + if(leftPart.charAt(0) == '.' || leftPart.charAt(0) == '?' || leftPart.charAt(0) == '!') { + leftPart = leftPart.substring(1, leftPart.length()); + leftPart = leftPart.trim(); + } + String[] first = leftPart.split(" "); + String rightPart = getPartQuote("" + phrases[phrases.length-1], forRightPart, "", "right"); + String[] last = rightPart.split(" "); + + leftPart = cutSentence(leftPart, first.length - (MAX_WORDS_ON_BOUNDARIES + 1), first.length, "left", first); + rightPart = cutSentence(rightPart, 0, MAX_WORDS_ON_BOUNDARIES + 1, "right", last); + + String textQuote = createTextQuote(phrases, leftPart, rightPart); + if (textQuote.isEmpty()) { + textQuote = item.getTaggedContent(); + } /*else { + textQuote = termsMarker.mark(textQuote); + }*/ + Quote quote = new Quote(); + quote.setNumber(item.getNumber()); + textQuote = textQuote.replaceAll("^[^<]+\\s*", "..."); + quote.setQuote(textQuote); + quotes.add(quote); + } + return quotes; + } + + String getPartQuote(String content, String regexp, String text, String flag) { + Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + Matcher matcher = pattern.matcher(content); + if(matcher.find()) { + text = matcher.group(); + } + + if(flag.equals("left")) { + if(brackets.contains(text.substring(0, 2))) { + text = skipBracketOrDirectSpeech(content, text, text.substring(2, text.length()), 2); + } + else if(directSpeech.contains(text.substring(0, 3))) { + text = skipBracketOrDirectSpeech(content, text, text.substring(3, text.length()), 3); + } + } + + if(flag.equals("right") && content.length() > text.length()) { + String sub = content.substring(text.length(), content.length()); + if(sub.startsWith(")") || sub.startsWith("»") || (sub.length() > 2 && isHyphen(sub.substring(0, 2)))) { + text += getPartQuote(content.substring(text.length(), content.length()), + forCreateRightPartQuote, text, "right"); + } + } + return text; + } + + private String skipBracketOrDirectSpeech(String content, String text, String temp, int skip) { + if(content.length() - text.length() > 0) { + text = getPartQuote(content.substring(0, (content.length() - text.length()) + skip), + forCreateLeftPartQuote, text.substring(2, text.length()), "left"); + + int offset = 0; + if(content.contains(text)) { + offset = content.indexOf(text); + } + text += content.substring(text.length() + offset, content.length() - temp.length()); + } + text += temp; + return text; + } + + private String cutSentence(String text, int startIndex, int endIndex, String flag, String[] words) { + String partText = ""; + if(words.length > MAX_WORDS_ON_BOUNDARIES + 1) { + for(int i = startIndex; i < endIndex; i++) { + partText += words[i] + " "; + } + if (flag.equals("left")) { + partText = partText.trim(); + text = "..." + partText.substring(0, partText.length() - 8).trim(); + } + if(flag.equals("right")) { + text = partText.trim() + "..."; + } + } + else if(words.length <= MAX_WORDS_ON_BOUNDARIES + 1 && flag.equals("left")) { + text = text.substring(0, text.length() - 8).trim(); + } + return text; + } + + private String createTextQuote(String[] phrases, String firstPart, String lastPart) { + String textQuote = firstPart; + for (int i = 1; i < phrases.length - 1; i++) { + textQuote += (textQuote.isEmpty() || + openQuoteBracketHyphen.contains(textQuote.charAt(textQuote.length()-1)) ? "" : " ") + + "" + phrases[i].trim(); + } + + if(!textQuote.isEmpty() && openQuoteBracketHyphen.contains(textQuote.charAt(textQuote.length()-1))) { + textQuote += lastPart; + } + else { + textQuote += " " + lastPart; + } + return textQuote.trim(); + } + + private boolean isHyphen(String text) { + char hyphen = text.charAt(1); + return (int) hyphen == 45 || (int) hyphen == 8211; } } + + diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/CacheEntity.java b/src/main/java/org/ayfaar/app/controllers/search/cache/CacheEntity.java new file mode 100644 index 00000000..f9b5c3b0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/CacheEntity.java @@ -0,0 +1,31 @@ +package org.ayfaar.app.controllers.search.cache; + + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ayfaar.app.model.UID; + +import javax.persistence.*; + +@Data +@NoArgsConstructor +@Entity +@Table(name= "cache") +public class CacheEntity { + @Id + @Column(name = "uid") + private String uri; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "uid", insertable = false, updatable = false) + private UID uid; + + @Column(name = "content", columnDefinition = "MEDIUMTEXT") + private String content; + + public CacheEntity(UID uid, String content) { + this.uri = uid.getUri(); + this.uid = uid; + this.content = content; + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/ContentsCacheKey.java b/src/main/java/org/ayfaar/app/controllers/search/cache/ContentsCacheKey.java new file mode 100644 index 00000000..e92a8a09 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/ContentsCacheKey.java @@ -0,0 +1,10 @@ +package org.ayfaar.app.controllers.search.cache; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; + +@AllArgsConstructor +@EqualsAndHashCode +public class ContentsCacheKey { + public final String categoryName; +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/DBCache.java b/src/main/java/org/ayfaar/app/controllers/search/cache/DBCache.java new file mode 100644 index 00000000..7a03f633 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/DBCache.java @@ -0,0 +1,103 @@ +package org.ayfaar.app.controllers.search.cache; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ayfaar.app.dao.CategoryDao; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Category; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.UriGenerator; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Optional; + + +@Component +public class DBCache extends ConcurrentMapCache { + @Inject ObjectMapper objectMapper; + @Inject TermService termService; + @Inject CommonDao commonDao; + @Inject CategoryDao categoryDao; + + public boolean disabled; + + public DBCache() { + super("DBCache"); + } + + + @Override + public ValueWrapper get(Object key) { + if (disabled) return super.get(key); + + ValueWrapper value = super.get(key); + if(value != null) { + return value; + } + + CacheEntity cacheEntity = null; + if (key instanceof SearchCacheKey) { + SearchCacheKey searchKey = (SearchCacheKey) key; + boolean isTerm = false; + if (searchKey.page == 0 && (searchKey.startFrom == null || searchKey.startFrom.isEmpty())) { + final Optional providerOpt = termService.getMainOrThis(searchKey.query); + String termUri = null; + if (providerOpt.isPresent()) { + termUri = providerOpt.get().getUri(); + } + if (termUri != null) { + cacheEntity = commonDao.get(CacheEntity.class, termUri); + isTerm = true; + } + } + if (!isTerm && searchKey.query.indexOf("Обсуждение:") != 0 && searchKey.query.indexOf("_") != 0) { +// eventPublisher.publishEvent(new SearchEvent(searchKey)); + } + + } else if(key instanceof ContentsCacheKey) { + final Category category = categoryDao.get("uri", + UriGenerator.generate(Category.class, ((ContentsCacheKey) key).categoryName)); + if(category != null) { + cacheEntity = commonDao.get(CacheEntity.class, "uri", category.getUri()); + } + } + + if (cacheEntity != null) { + put(key, cacheEntity.getContent()); + value = new SimpleValueWrapper(cacheEntity.getContent()); + } + return value; + } + + @Override + public void put(Object key, Object value) { + if (disabled) return; + + String json; + UID uid = null; + + try { + json = (value instanceof String) ? (String)value : objectMapper.writeValueAsString(value); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (key instanceof SearchCacheKey && ((SearchCacheKey) key).page == 0) { + Optional providerOpt = termService.getMainOrThis(((SearchCacheKey) key).query); + if(providerOpt.isPresent() && ((SearchCacheKey) key).page == 0) { + uid = providerOpt.get().getTerm(); + } + } else if(key instanceof ContentsCacheKey) { + String name = ((ContentsCacheKey) key).categoryName; + uid = commonDao.get(UID.class, UriGenerator.generate(Category.class, name)); + } + if(uid != null) { + commonDao.save(new CacheEntity(uid, json)); + } + super.put(key, json); + } +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/DBCacheManager.java b/src/main/java/org/ayfaar/app/controllers/search/cache/DBCacheManager.java new file mode 100644 index 00000000..b1d7c637 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/DBCacheManager.java @@ -0,0 +1,23 @@ +package org.ayfaar.app.controllers.search.cache; + + +import org.springframework.cache.Cache; +import org.springframework.cache.support.AbstractCacheManager; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.Collection; + +import static java.util.Arrays.asList; + +@Component +public class DBCacheManager extends AbstractCacheManager { + + @Inject DBCache dbCache; + + @Override + protected Collection loadCaches() { + return asList(dbCache); + } + +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheKey.java b/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheKey.java new file mode 100644 index 00000000..93e60b43 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheKey.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.controllers.search.cache; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; + +@AllArgsConstructor +@EqualsAndHashCode +public class SearchCacheKey { + public final String query; + public final String startFrom; + public final Integer page; +} diff --git a/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheUpdater.java b/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheUpdater.java new file mode 100644 index 00000000..caa8f820 --- /dev/null +++ b/src/main/java/org/ayfaar/app/controllers/search/cache/SearchCacheUpdater.java @@ -0,0 +1,76 @@ +package org.ayfaar.app.controllers.search.cache; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.time.DurationFormatUtils; +import org.ayfaar.app.controllers.CategoryController; +import org.ayfaar.app.controllers.NewSearchController; +import org.ayfaar.app.controllers.search.SearchResultPage; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Category; +import org.ayfaar.app.model.Term; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.UriGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.List; + +import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; + +@Component +@EnableScheduling +@Profile("default") +@Slf4j +public class SearchCacheUpdater { + @Autowired + private NewSearchController searchController; + @Autowired + private CategoryController categoryController; + @Autowired + private CommonDao commonDao; + @Autowired + private TermService termService; + @Inject ObjectMapper objectMapper; + + @Scheduled(fixedDelay = 604800000, initialDelay = 3*360000) // обновлять кеш спустя 3 часа со старта и через неделю после каждого завершения обновления + public void update() throws IOException { + long start = System.currentTimeMillis(); + log.info("Search cache updating started"); + termService.reload(); + updateCacheSearchResult(); + + long end = System.currentTimeMillis(); + final String duration = DurationFormatUtils.formatDuration(end - start, "HH:mm:ss"); + log.info("Search catch updated in " + duration); + } + + private void updateCacheSearchResult() throws IOException { + //clean cache for search results + + String uri = UriGenerator.generate(Term.class, ""); + List searchCacheList = commonDao.getLike(CacheEntity.class, "uri", uri + "%", Integer.MAX_VALUE); + + for (CacheEntity cache : searchCacheList) { + final SearchResultPage searchResult = (SearchResultPage) searchController.searchWithoutCache( + getValueFromUri(Term.class, cache.getUri()), 0, null); + cache.setContent(objectMapper.writeValueAsString(searchResult)); + commonDao.save(cache); + } + } + + public void update(String uri) { + //clean cache by uri + + if(uri.startsWith(UriGenerator.generate(Term.class, ""))) { + searchController.search(getValueFromUri(Term.class, uri), 0, null); + } else if(uri.startsWith(UriGenerator.generate(Category.class, ""))) { + categoryController.getContents(getValueFromUri(Category.class, uri)); + } + } +} diff --git a/src/main/java/org/ayfaar/app/dao/BasicCrudDao.java b/src/main/java/org/ayfaar/app/dao/BasicCrudDao.java index 45953bb3..3c1bd70e 100644 --- a/src/main/java/org/ayfaar/app/dao/BasicCrudDao.java +++ b/src/main/java/org/ayfaar/app/dao/BasicCrudDao.java @@ -4,6 +4,7 @@ import org.hibernate.criterion.MatchMode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.data.domain.Pageable; import java.io.Serializable; import java.util.List; @@ -63,6 +64,8 @@ public interface BasicCrudDao { List getLike(String property, @NotNull String value, MatchMode matchMode, int limit); + List getLike(String property, @NotNull List values, MatchMode matchMode); + /** * SQL: ...WHERE REGEXP * @@ -89,6 +92,8 @@ public interface BasicCrudDao { List getPage(int skip, int pageSize, String sortField, String sortDirection); @NotNull List getPage(int skip, int pageSize, String sortField, String sortDirection, List aliases, List criterions); + @NotNull + List getPage(Pageable pageable); @NotNull Long getCount(); @@ -110,10 +115,10 @@ public interface BasicCrudDao { E getOneFor(String entity, Serializable id); List getByExample(E o); - +/* List getAudit(Serializable id); - List getAllAudit(); + List getAllAudit();*/ E initialize(E detachedParent, String fieldName); } diff --git a/src/main/java/org/ayfaar/app/dao/CommonDao.java b/src/main/java/org/ayfaar/app/dao/CommonDao.java index ed341645..794080f7 100644 --- a/src/main/java/org/ayfaar/app/dao/CommonDao.java +++ b/src/main/java/org/ayfaar/app/dao/CommonDao.java @@ -1,22 +1,29 @@ package org.ayfaar.app.dao; import org.ayfaar.app.utils.Content; -import org.hibernate.envers.AuditReader; -import org.hibernate.envers.RevisionType; +import org.hibernate.Criteria; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.io.Serializable; -import java.util.Collection; import java.util.List; +import java.util.Optional; public interface CommonDao { List getAll(Class clazz); + Optional getOpt(Class clazz, Serializable id); @Nullable + @Deprecated + // use getOpt() instead E get(Class clazz, Serializable id); + @Deprecated E get(Class clazz, String property, Object value); + // use getOpt() instead + Optional getOpt(Class clazz, String property, Object value); E save(Class entityClass, E entity); E save(E entity); @@ -25,27 +32,33 @@ public interface CommonDao { void remove(Class entityClass, Serializable id); void remove(Object entity); - E getByCode(Class entity, String code); - List getList(Class clazz, String property, Object value); + List getList(Class clazz, String property, Object value, Pageable pageable); + + List getList(Class clazz, Pageable pageable); + + List getListWithout(Class clazz, String property, Object value, Pageable pageable); + List getFor(Class clazz, String entity, Serializable id); E getSingleFor(Class clazz, String entity, Serializable id); - E getRandom(Class clazz); - E initialize(Class className, E detachedParent, String fieldName); -// List findInAllContent(String query); - + @Deprecated List findInAllContent(String query, Integer start, Integer pageSize); + @Deprecated List findInAllContent(List aliases, Integer start, Integer pageSize); List getLike(Class className, String field, String value, Integer limit); - AuditReader getAuditReader(); + @NotNull + List getPage(Class entityClass, Pageable pageable); + + @NotNull + List getPage(Class entityClass, int skip, int pageSize, String sortField, String sortDirection); - Collection findAuditEntities(Number revision, RevisionType revisionType); + Criteria getCriteria(Class entityClass, Pageable pageable); } diff --git a/src/main/java/org/ayfaar/app/dao/ItemDao.java b/src/main/java/org/ayfaar/app/dao/ItemDao.java index 31bb2f05..a7dc6804 100644 --- a/src/main/java/org/ayfaar/app/dao/ItemDao.java +++ b/src/main/java/org/ayfaar/app/dao/ItemDao.java @@ -2,8 +2,17 @@ import org.ayfaar.app.model.Item; +import java.util.List; + + public interface ItemDao extends BasicCrudDao { Item getByNumber(String number); + List getAllNumbers(); + + List getNext(String number, Integer more); + + String getTomLastItemNumber(String tom); + // List find(String query); } diff --git a/src/main/java/org/ayfaar/app/dao/ItemsRangeDao.java b/src/main/java/org/ayfaar/app/dao/ItemsRangeDao.java new file mode 100644 index 00000000..5bd9dd4d --- /dev/null +++ b/src/main/java/org/ayfaar/app/dao/ItemsRangeDao.java @@ -0,0 +1,9 @@ +package org.ayfaar.app.dao; + +import org.ayfaar.app.model.ItemsRange; + +import java.util.List; + +public interface ItemsRangeDao extends BasicCrudDao { + List getWithCategories(); +} diff --git a/src/main/java/org/ayfaar/app/dao/LinkDao.java b/src/main/java/org/ayfaar/app/dao/LinkDao.java index 3372ee12..1b50cb79 100644 --- a/src/main/java/org/ayfaar/app/dao/LinkDao.java +++ b/src/main/java/org/ayfaar/app/dao/LinkDao.java @@ -19,4 +19,8 @@ public interface LinkDao extends BasicCrudDao { List getRelatedWithQuote(String uri); List getByUris(String uri1, String uri2); + + List get(UID uid1, UID uid2); + + List getAllSynonyms(); } diff --git a/src/main/java/org/ayfaar/app/dao/RecordDao.java b/src/main/java/org/ayfaar/app/dao/RecordDao.java new file mode 100644 index 00000000..d7d00867 --- /dev/null +++ b/src/main/java/org/ayfaar/app/dao/RecordDao.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.dao; + + +import org.ayfaar.app.model.Record; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface RecordDao extends BasicCrudDao{ + + List get(String nameOrCode, String year, Record.Kind kind, boolean isUrlPresent, Pageable pageable); +} diff --git a/src/main/java/org/ayfaar/app/dao/SearchDao.java b/src/main/java/org/ayfaar/app/dao/SearchDao.java index aabb9605..63cae5b1 100644 --- a/src/main/java/org/ayfaar/app/dao/SearchDao.java +++ b/src/main/java/org/ayfaar/app/dao/SearchDao.java @@ -5,8 +5,5 @@ import java.util.List; public interface SearchDao extends BasicCrudDao { - public List searchInDb(String query, int skipResults, int maxResults, String fromItemNumber); - public List searchInDb(List words, int skipResults, int maxResults, String fromItemNumber); - public List findInItems(List aliases, int skip, int limit); - public List testFilter(List aliases, int skip, int limit, String filter); + public List findInItems(List aliases, int skip, int limit, String startFrom); } diff --git a/src/main/java/org/ayfaar/app/dao/SongDao.java b/src/main/java/org/ayfaar/app/dao/SongDao.java deleted file mode 100644 index ba2e2190..00000000 --- a/src/main/java/org/ayfaar/app/dao/SongDao.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.ayfaar.app.dao; - -import org.ayfaar.app.model.Song; - -public interface SongDao extends BasicCrudDao { - Song getByName(String name); - Song getById(Integer songId); - Song getSongHtml(Integer songId); -} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/dao/TermDao.java b/src/main/java/org/ayfaar/app/dao/TermDao.java index ae0a5eeb..25c71192 100644 --- a/src/main/java/org/ayfaar/app/dao/TermDao.java +++ b/src/main/java/org/ayfaar/app/dao/TermDao.java @@ -1,7 +1,9 @@ package org.ayfaar.app.dao; +import lombok.Data; import org.ayfaar.app.model.Term; + import java.util.List; public interface TermDao extends BasicCrudDao { @@ -10,4 +12,17 @@ public interface TermDao extends BasicCrudDao { List getLike(String field, String value); List getGreaterThan(String field, Object value); + + List getAllTermInfo(); + + @Data + public static class TermInfo { + private String name; + private boolean hasShortDescription; + + public TermInfo(String name, boolean hasShortDescription) { + this.name = name; + this.hasShortDescription = hasShortDescription; + } + } } diff --git a/src/main/java/org/ayfaar/app/dao/impl/AbstractHibernateDAO.java b/src/main/java/org/ayfaar/app/dao/impl/AbstractHibernateDAO.java index c50728be..1709f91e 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/AbstractHibernateDAO.java +++ b/src/main/java/org/ayfaar/app/dao/impl/AbstractHibernateDAO.java @@ -1,21 +1,18 @@ package org.ayfaar.app.dao.impl; import org.ayfaar.app.dao.BasicCrudDao; -import org.dozer.DozerBeanMapper; import org.hibernate.*; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.hibernate.cfg.ImprovedNamingStrategy; import org.hibernate.criterion.*; -import org.hibernate.envers.AuditReader; -import org.hibernate.envers.AuditReaderFactory; -import org.hibernate.envers.query.AuditEntity; -import org.hibernate.envers.query.AuditQuery; import org.hibernate.metadata.ClassMetadata; import org.hibernate.type.StringType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -68,6 +65,24 @@ protected Criteria criteria() { return currentSession().createCriteria(entityClass); } + protected Criteria criteria(Pageable pageable) { + final Sort sort = pageable.getSort(); + Optional order = Optional.ofNullable(sort != null && sort.iterator().hasNext() ? sort.iterator().next() : null); + + Criteria criteria = currentSession().createCriteria(entityClass) + .setFirstResult(pageable.getOffset()) + .setMaxResults(pageable.getPageSize()); + + String sortField = order.isPresent() ? order.get().getProperty() : null; + String sortDirection = order.isPresent() ? order.get().getDirection().name() : null; + + if (sortField != null && !sortField.isEmpty()) { + criteria.addOrder("asc".equals(sortDirection) ? Order.asc(sortField) : Order.desc(sortField)); + } + + return criteria; + } + protected Query query(String hql) { return currentSession().createQuery(hql); } @@ -101,7 +116,7 @@ protected List list(Criteria criteria) { @SuppressWarnings("unchecked") protected List list(Criteria criteria, boolean cache) { criteria.setCacheable(cache); - return new ArrayList(new LinkedHashSet(criteria.list())); // privent duplications + return new ArrayList(criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list()); // privent duplications } protected List list(Query query) { @@ -175,6 +190,19 @@ public List getLike(String property, @NotNull String value, MatchMode matchMo .list(); } + @Override + public List getLike(String property, @NotNull List values, MatchMode matchMode) { + Criteria criteria = criteria(); + Disjunction disjunction = Restrictions.disjunction(); + + for (String alias : values) { + disjunction.add(like(property, alias, matchMode)); + } + criteria.add(disjunction); + + return criteria.list(); + } + @Override public List getByRegexp(String property, String regexp) { return criteria() @@ -201,10 +229,12 @@ public void remove(Serializable id) { currentSession().delete(entity); } + @NotNull public List getPage(int skip, int pageSize) { return list(criteria().setFirstResult(skip).setMaxResults(pageSize)); } + @NotNull public List getPage(int skip, int pageSize, String sortField, String sortDirection) { Criteria criteria = currentSession().createCriteria(entityClass) .setFirstResult(skip) @@ -238,6 +268,13 @@ public List getPage(int skip, int pageSize, String sortField, String sortDire return list(criteria); } + @NotNull + @Override + public List getPage(Pageable pageable) { + return list(criteria(pageable)); + } + + @NotNull public Long getCount() { return (Long) currentSession().createQuery("select count (*) from "+entityClass.getName()) .uniqueResult(); @@ -268,6 +305,7 @@ public Long getCount(Set filter) { return (Long) criteria.uniqueResult(); } + @NotNull @Override public List getAll() { return criteria() @@ -311,7 +349,7 @@ public List getByExample(E o) { return list(criteria().add(Example.create(o))); } - @Override + /*@Override public List getAudit(Serializable id) { AuditReader reader = AuditReaderFactory.get(currentSession()); @@ -332,9 +370,9 @@ public List getAudit(Serializable id) { } return result; - } + }*/ - @Override +/* @Override public List getAllAudit() { AuditReader reader = AuditReaderFactory.get(currentSession()); @@ -350,7 +388,7 @@ public List getAllAudit() { } return result; - } + }*/ @Override public E initialize(E detachedParent, String fieldName) { diff --git a/src/main/java/org/ayfaar/app/dao/impl/CommonDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/CommonDaoImpl.java index f2b8259e..8f4fefea 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/CommonDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/CommonDaoImpl.java @@ -5,22 +5,23 @@ import org.hibernate.Criteria; import org.hibernate.Hibernate; import org.hibernate.SessionFactory; +import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; -import org.hibernate.envers.AuditReader; -import org.hibernate.envers.AuditReaderFactory; -import org.hibernate.envers.RevisionType; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import static org.apache.commons.beanutils.PropertyUtils.setProperty; import static org.ayfaar.app.utils.EntityUtils.getPrimaryKeyFiledName; @@ -43,16 +44,15 @@ public List getAll(Class clazz) { } @Override - public E getRandom(Class clazz) { - return (E) sessionFactory.getCurrentSession().createCriteria(clazz) - .add(Restrictions.sqlRestriction("1=1 order by rand()")) - .setMaxResults(1) - .list().get(0); + public Optional getOpt(Class clazz, Serializable id) { + return Optional.ofNullable(get(clazz, id)); } @Nullable @Override public E get(Class clazz, Serializable id) { + Assert.notNull(clazz); + Assert.notNull(id); return (E) sessionFactory.getCurrentSession().get(clazz, id); } @@ -63,6 +63,11 @@ public E get(Class clazz, String property, Object value) { .uniqueResult(); } + @Override + public Optional getOpt(Class clazz, String property, Object value) { + return Optional.ofNullable(get(clazz, property, value)); + } + @Override public List getList(Class clazz, String property, Object value) { return sessionFactory.getCurrentSession() @@ -70,6 +75,25 @@ public List getList(Class clazz, String property, Object value) { .list(); } + @Override + public List getList(Class clazz, String property, Object value, Pageable pageable) { + return criteria(clazz, pageable) + .add(value == null ? isNull(property) : eq(property, value)) + .list(); + } + + @Override + public List getList(Class clazz, Pageable pageable) { + return criteria(clazz, pageable).list(); + } + + @Override + public List getListWithout(Class clazz, String property, Object value, Pageable pageable) { + return criteria(clazz, pageable) + .add(value == null ? isNotNull(property) : Restrictions.ne(property, value)) + .list(); + } + @Override public List getFor(Class clazz, String entity, Serializable id) { return list(sessionFactory.getCurrentSession().createCriteria(clazz) @@ -119,16 +143,8 @@ public void remove(Object entity) { remove(entity.getClass(), (Serializable) getPrimaryKeyValue(entity)); } - @Override - public E getByCode(Class className, String code) { - return (E) sessionFactory.getCurrentSession() - .createCriteria(className) - .add(eq("code", code)) - .uniqueResult(); - } - protected List list(Criteria criteria) { - return new ArrayList(new LinkedHashSet(criteria.list())); // privent duplications + return new ArrayList(criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).list()); } @Override @@ -212,27 +228,53 @@ public List getLike(Class className, String field, String value, Integ ); } + @NotNull @Override - public AuditReader getAuditReader() { - return AuditReaderFactory.get(sessionFactory.getCurrentSession()); - - /*AuditQuery query = reader.getCrossTypeRevisionChangesReader().findEntities(); - - List result = new ArrayList(); - List audits = query.getResultList(); + public List getPage(Class entityClass, Pageable pageable) { + final Sort sort = pageable.getSort(); + Optional order = Optional.ofNullable(sort != null && sort.iterator().hasNext() ? sort.iterator().next() : null); + return getPage( + entityClass, + pageable.getOffset(), + pageable.getPageSize(), + order.isPresent() ? order.get().getProperty() : null, + order.isPresent() ? order.get().getDirection().name() : null); + } - DozerBeanMapper mapper = new DozerBeanMapper(); + @NotNull + @Override + public List getPage(Class entityClass, int skip, int pageSize, String sortField, String sortDirection) { + Criteria criteria = sessionFactory.getCurrentSession().createCriteria(entityClass) + .setFirstResult(skip) + .setMaxResults(pageSize); - for (Object[] objects : audits) { - result.add(mapper.map(objects[0], entityClass)); + if (sortField != null && !sortField.isEmpty()) { + criteria.addOrder("asc".equals(sortDirection) ? Order.asc(sortField) : Order.desc(sortField)); } - return result;*/ + return list(criteria); } @Override - public Collection findAuditEntities(Number revision, RevisionType revisionType) { - return AuditReaderFactory.get(sessionFactory.getCurrentSession()) - .getCrossTypeRevisionChangesReader().findEntities(revision, revisionType); + public Criteria getCriteria(Class entityClass, Pageable pageable) { + return criteria(entityClass, pageable); + } + + protected Criteria criteria(Class entityClass, Pageable pageable) { + final Sort sort = pageable.getSort(); + Optional order = Optional.ofNullable(sort != null && sort.iterator().hasNext() ? sort.iterator().next() : null); + + Criteria criteria = sessionFactory.getCurrentSession().createCriteria(entityClass) + .setFirstResult(pageable.getOffset()) + .setMaxResults(pageable.getPageSize()); + + String sortField = order.isPresent() ? order.get().getProperty() : null; + String sortDirection = order.isPresent() ? order.get().getDirection().name() : null; + + if (sortField != null && !sortField.isEmpty()) { + criteria.addOrder("asc".equals(sortDirection) ? Order.asc(sortField) : Order.desc(sortField)); + } + + return criteria; } } \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/dao/impl/ItemDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/ItemDaoImpl.java index ce327665..d6b23aac 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/ItemDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/ItemDaoImpl.java @@ -2,8 +2,14 @@ import org.ayfaar.app.dao.ItemDao; import org.ayfaar.app.model.Item; +import org.hibernate.criterion.Order; +import org.hibernate.criterion.Projections; +import org.hibernate.criterion.Restrictions; import org.springframework.stereotype.Repository; +import java.util.List; + + @SuppressWarnings("unchecked") @Repository public class ItemDaoImpl extends AbstractHibernateDAO implements ItemDao { @@ -16,6 +22,28 @@ public Item getByNumber(String number) { return get("number", number); } + @Override + public List getAllNumbers() { + return criteria().setProjection(Projections.property("number")).list(); + } + + @Override + public List getNext(String number, Integer more) { + return (List) criteria() + .add(Restrictions.gt("number", number)) + .addOrder(Order.asc("orderIndex")) + .setMaxResults(more) + .list(); + } + + @Override + public String getTomLastItemNumber(String tom) { + return (String) criteria() + .add(Restrictions.like("number", tom + ".%")) + .setProjection(Projections.max("number")) + .uniqueResult(); + } + /*@Override public List find(String query) { query = query.toLowerCase().replaceAll("\\*", "["+w+"]*"); diff --git a/src/main/java/org/ayfaar/app/dao/impl/ItemsRangeDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/ItemsRangeDaoImpl.java new file mode 100644 index 00000000..76b42948 --- /dev/null +++ b/src/main/java/org/ayfaar/app/dao/impl/ItemsRangeDaoImpl.java @@ -0,0 +1,22 @@ +package org.ayfaar.app.dao.impl; + +import org.ayfaar.app.dao.ItemsRangeDao; +import org.ayfaar.app.model.ItemsRange; +import org.hibernate.criterion.Restrictions; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class ItemsRangeDaoImpl extends AbstractHibernateDAO implements ItemsRangeDao { + public ItemsRangeDaoImpl() { + super(ItemsRange.class); + } + + @Override + public List getWithCategories() { + return criteria() + .add(Restrictions.isNotNull("category")) + .list(); + } +} diff --git a/src/main/java/org/ayfaar/app/dao/impl/LinkDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/LinkDaoImpl.java index 9dbc71fd..38a9e1bc 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/LinkDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/LinkDaoImpl.java @@ -2,6 +2,7 @@ import org.ayfaar.app.dao.LinkDao; import org.ayfaar.app.model.Link; +import org.ayfaar.app.model.LinkType; import org.ayfaar.app.model.UID; import org.hibernate.Criteria; import org.hibernate.criterion.Order; @@ -9,6 +10,9 @@ import java.util.List; +import static org.ayfaar.app.model.LinkType.ABBREVIATION; +import static org.ayfaar.app.model.LinkType.ALIAS; +import static org.ayfaar.app.model.LinkType.CODE; import static org.hibernate.criterion.Restrictions.*; @SuppressWarnings("unchecked") @@ -24,7 +28,7 @@ public List getAliases(String uri) { .createAlias("uid1", "uid1") .createAlias("uid2", "uid2") .add(eq("uid1.uri", uri)) - .add(or(in("type", new Object[] {Link.ALIAS, Link.ABBREVIATION, Link.CODE}))) + .add(or(in("type", new Object[] {ALIAS, ABBREVIATION, CODE}))) .list(); } @@ -34,7 +38,7 @@ public UID getPrimeForAlias(String uri) { .createAlias("uid1", "uid1") .createAlias("uid2", "uid2") .add(eq("uid2.uri", uri)) - .add(or(in("type", new Object[] {Link.ALIAS, Link.ABBREVIATION, Link.CODE}))) + .add(or(in("type", new Object[] {ALIAS, ABBREVIATION, CODE}))) .uniqueResult(); if (link != null) { return link.getUid1(); @@ -53,7 +57,7 @@ private Criteria getRelatedCriteria(String uri) { .createAlias("uid1", "uid1") .createAlias("uid2", "uid2") .add(or(eq("uid1.uri", uri), eq("uid2.uri", uri))) - .add(or(in("type", new Byte[]{Link.ALIAS, Link.CODE}), isNull("type"))) + .add(or(in("type", new LinkType[]{ALIAS, CODE}), isNull("type"))) // .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) .addOrder(Order.desc("weight")); } @@ -72,7 +76,7 @@ public Link getForAbbreviationOrAliasOrCode(String uri) { return (Link) criteria() .createAlias("uid2", "uid2") .add(eq("uid2.uri", uri)) - .add(in("type", new Byte[]{Link.ABBREVIATION, Link.ALIAS, Link.CODE})) + .add(in("type", new LinkType[]{ABBREVIATION, ALIAS, CODE})) .uniqueResult(); } @@ -96,4 +100,21 @@ public List getByUris(String uri1, String uri2) { ) .list(); } + + @Override + public List get(UID uid1, UID uid2) { + return criteria() + .add( + or( + and(eq("uid1", uid1), eq("uid2", uid2)), + and(eq("uid1", uid2), eq("uid2", uid1)) + ) + ) + .list(); + } + + @Override + public List getAllSynonyms() { + return criteria().add(in("type", new LinkType[]{ABBREVIATION, ALIAS, CODE})).list(); + } } diff --git a/src/main/java/org/ayfaar/app/dao/impl/RecordDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/RecordDaoImpl.java new file mode 100644 index 00000000..50618bdd --- /dev/null +++ b/src/main/java/org/ayfaar/app/dao/impl/RecordDaoImpl.java @@ -0,0 +1,45 @@ +package org.ayfaar.app.dao.impl; + +import org.ayfaar.app.dao.RecordDao; +import org.ayfaar.app.model.Record; +import org.hibernate.Criteria; +import org.hibernate.criterion.MatchMode; +import org.hibernate.criterion.Restrictions; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import java.util.List; + +import static org.hibernate.criterion.Restrictions.like; + +@Repository +public class RecordDaoImpl extends AbstractHibernateDAO implements RecordDao { + + public RecordDaoImpl() { + super(Record.class); + } + + @Override + public List get(String nameOrCode, String year, Record.Kind kind, boolean isUrlPresent, Pageable pageable){ + + Criteria criteria = criteria(pageable); + + if (nameOrCode != null && !nameOrCode.isEmpty()) + criteria.add(Restrictions.or( + like("code", nameOrCode, MatchMode.ANYWHERE), + like("name", nameOrCode, MatchMode.ANYWHERE))); + + if (year != null && !year.isEmpty()) criteria.add(like("code", year, MatchMode.ANYWHERE)); + if (isUrlPresent) criteria.add(Restrictions.isNotNull("audioUrl")); + + if (kind != null) { + if (kind == Record.Kind.k) { + criteria.add(like("code", "-k", MatchMode.END)); + } else { + criteria.add(Restrictions.not(like("code", "-k", MatchMode.END))); + criteria.add(Restrictions.not(like("code", "-m", MatchMode.END))); + } + } + + return criteria.list(); + } +} diff --git a/src/main/java/org/ayfaar/app/dao/impl/SearchDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/SearchDaoImpl.java index 270be9ea..940ecb31 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/SearchDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/SearchDaoImpl.java @@ -1,90 +1,60 @@ package org.ayfaar.app.dao.impl; -import org.apache.commons.lang.NotImplementedException; import org.ayfaar.app.dao.SearchDao; import org.ayfaar.app.model.Item; import org.hibernate.Criteria; +import org.hibernate.HibernateException; import org.hibernate.criterion.*; import org.springframework.stereotype.Repository; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import static java.util.Arrays.asList; -import static org.hibernate.criterion.Restrictions.ge; -import static org.hibernate.criterion.Restrictions.like; +import static org.ayfaar.app.utils.LikeExpression.like; @Repository public class SearchDaoImpl extends AbstractHibernateDAO implements SearchDao { + private boolean hasMore = false; public SearchDaoImpl() { super(Item.class); } - public List searchInDb(String query, int skipResults, int maxResults, String fromItemNumber) { - return searchInDb(asList(query), skipResults, maxResults, fromItemNumber); + public boolean hasMoreItems() { + return hasMore; } - public List searchInDb(List words, int skipResults, int maxResults, String fromItemNumber) { - // 4.1. Результат должен быть отсортирован: - // Сначала самые ранние пункты - // 4.2. Если filter заполнен то нужно учесть стартовый и конечный абзаци - // 4.3. В результате нужно знать есть ли ещё результаты поиска для следующей страницы - - - throw new NotImplementedException(); - } - - public List findInItems(List aliases, int skip, int limit) { - Criteria criteria = criteria(); + public List findInItems(List aliases, int skip, int limit, String startFrom) { + DetachedCriteria idsCriteria = DetachedCriteria.forClass(Item.class);//criteria(); Disjunction disjunction = Restrictions.disjunction(); + if(startFrom != null && !startFrom.isEmpty() && !startFrom.equals("undefined")) { + idsCriteria.add(new SimpleExpression("orderIndex", Float.valueOf(startFrom), ">=") { + @Override + public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { + return " order_index >= ?"; + } + }); + idsCriteria.addOrder(Order.asc("orderIndex")); + } + for (String alias : aliases) { - for (char startChar : new char[]{'-', ' ', '(', '«'}) {//fixme почему бы сюда не добавить '' и тогда нужен второй цикл - for (char endChar : new char[]{'?', '!', ',', '.', ' ', '"', ';', ':', ')', '»'}) { - disjunction.add(like("content", startChar + alias + endChar, MatchMode.ANYWHERE)); + for (char endChar : new char[]{'?', '!', ',', '.', ' ', '"', ';', ':', ')', '»', '-'}) { + for (char startChar : new char[]{'-', ' ', '(', '«'}) { +// disjunction.add(like("content", startChar + alias + endChar, MatchMode.ANYWHERE)); + disjunction.add(like("content", "%"+startChar + alias + endChar+"%", '\\')); } - } - //иногда фраза которую мы ищем стоит в самом начале пердложения и перед ней нет ни пробела, ни других знаков - for (char endChar : new char[]{'?', '!', ',', '.', ' ', '"', ';', ':', ')', '»'}) { - disjunction.add(like("content", alias + endChar, MatchMode.ANYWHERE)); + disjunction.add(like("content", alias + endChar+"%", '\\')); } } - criteria.add(disjunction).setMaxResults(limit).setFirstResult(skip); - - List sortedItems = new ArrayList(criteria.list()); - Collections.sort(sortedItems); - return sortedItems; - } - //fixme ну вроди всё верно, только ведь не нужно делать отдельный метод, просто добавь возможность фильтрации в findInItems - public List testFilter(List aliases, int skip, int limit, String filter) { - Criteria criteria = criteria(); - // зачем здесь дизьюнкция? ты же не используешь её - Disjunction disjunction = Restrictions.disjunction(); + idsCriteria.setProjection(Projections.distinct(Projections.id())); + idsCriteria.add(disjunction); - - criteria.add(ge("number", filter)).addOrder(new Order("number", true) { - @Override - public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) { - // хорошо реализовал - return "cast(number as decimal)"; - } - }); - //criteria.add(between("number", filter, "15.17876")); - criteria.add(disjunction).setMaxResults(limit).setFirstResult(skip); - //criteria.addOrder(Order.asc("doubleNumber")); - /*criteria.addOrder(new Order("number", true) { - @Override - public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) { - return "cast(number as decimal)"; - } - });*/ - // fixme зачем дополнительно сортируешь? - List sortedItems = new ArrayList(criteria.list()); - Collections.sort(sortedItems); - return sortedItems; + return criteria() + .add(Subqueries.propertyIn("uri", idsCriteria)) + .setMaxResults(limit) + .setFirstResult(skip) + .list(); } } diff --git a/src/main/java/org/ayfaar/app/dao/impl/SongDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/SongDaoImpl.java deleted file mode 100644 index fa4d2256..00000000 --- a/src/main/java/org/ayfaar/app/dao/impl/SongDaoImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.ayfaar.app.dao.impl; - -import org.ayfaar.app.dao.SongDao; -import org.ayfaar.app.model.Song; -import org.springframework.stereotype.Repository; - -import static org.hibernate.criterion.Restrictions.eq; -import static org.hibernate.criterion.Restrictions.ilike; - -@Repository -public class SongDaoImpl extends AbstractHibernateDAO implements SongDao { - public SongDaoImpl() { - super(Song.class); - } - - @Override - public Song getByName(String name) { - return (Song) criteria() - .add(ilike("name", name)) - .uniqueResult(); - } - - @Override - public Song getById(Integer songId) { - return (Song) criteria() - .add(eq("id", songId)) - .uniqueResult(); - } - - @Override - @Deprecated - public Song getSongHtml(Integer songId){ - String songHtml=""; - Song song = getById(songId); - String lines[] = song.getContent().split("\\n"); - - int tabs, spaces; - boolean inRepeatBlock_new = false, inRepeatBlock_old = false; - boolean inRefrain_new = false, inRefrain_old = false; - for(String line: lines) { - tabs = spaces = 0; - inRepeatBlock_old = inRepeatBlock_new; - inRefrain_old = inRefrain_new; - // first 7 symbols set the format of a html line: 2 tabs + 5 spaces - for (int i = 0; i < 7 && i < line.length(); i++) { - char c = line.charAt(i); - if(c == ' '){ - ++spaces; - } - else{ - if(c == '\t'){ - ++tabs; - } - else{ - break; - } - } - } - // refrain block - if(tabs == 1){ - inRefrain_new = true; - } - else{ - inRefrain_new = false; - } - // repeat block - if(tabs == 2){ - inRepeatBlock_new = true; - } - else{ - inRepeatBlock_new = false; - } - // handle refrain html-tags - if(inRefrain_new == true && inRefrain_old == false){ - songHtml += "
"; - } - if(inRefrain_new == false && inRefrain_old == true){ - songHtml += "
"; - } - // handle repeat html-tags - if(inRepeatBlock_new == true && inRepeatBlock_old == false){ - songHtml += "
"; - } - if(inRepeatBlock_new == false && inRepeatBlock_old == true){ - songHtml += "
"; - } - songHtml += "
"+ line.trim() + "
"; - } - // last line is in refrain block as well - if(inRefrain_new == true && inRefrain_old == true){ - songHtml += ""; - } - // last line is in repeat block as well - if(inRepeatBlock_new == true && inRepeatBlock_old == true){ - songHtml += ""; - } - song.setName(song.getName().replaceAll("\\n", "
")); - song.setInfo(song.getInfo().replaceAll("\\n", "
")); - song.setContent(songHtml); - return song; - } -} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/dao/impl/TermDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/TermDaoImpl.java index 43a2a348..c0782347 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/TermDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/TermDaoImpl.java @@ -5,8 +5,10 @@ import org.hibernate.criterion.Restrictions; import org.springframework.stereotype.Repository; +import java.util.ArrayList; import java.util.List; +import static org.hibernate.criterion.Restrictions.eq; import static org.hibernate.criterion.Restrictions.ilike; @Repository @@ -18,7 +20,7 @@ public TermDaoImpl() { @Override public Term getByName(String name) { return (Term) criteria() - .add(ilike("name", name)) + .add(eq("name", name)) .uniqueResult(); } @@ -36,4 +38,18 @@ public List getGreaterThan(String field, Object value) { .add(Restrictions.gt(field, value)) .list(); } + + @SuppressWarnings("unchecked") + @Override + public List getAllTermInfo() { + @SuppressWarnings("JpaQlInspection") List list = currentSession() + .createQuery("select t.name, case when t.shortDescription is null then 0 else 1 end from Term t") + .list(); + + List termsInfo = new ArrayList(); + for (Object[] info : list) { + termsInfo.add(new TermInfo((String) info[0], (Integer)info[1] == 1)); + } + return termsInfo; + } } diff --git a/src/main/java/org/ayfaar/app/dao/impl/TermMorphDaoImpl.java b/src/main/java/org/ayfaar/app/dao/impl/TermMorphDaoImpl.java index f586a7b2..3531ebee 100644 --- a/src/main/java/org/ayfaar/app/dao/impl/TermMorphDaoImpl.java +++ b/src/main/java/org/ayfaar/app/dao/impl/TermMorphDaoImpl.java @@ -7,7 +7,7 @@ import java.util.List; -import static org.hibernate.criterion.Restrictions.ilike; +import static org.hibernate.criterion.Restrictions.eq; @Repository public class TermMorphDaoImpl extends AbstractHibernateDAO implements TermMorphDao { @@ -18,7 +18,7 @@ public TermMorphDaoImpl() { @Override public TermMorph getByName(String name) { return (TermMorph) criteria() - .add(ilike("name", name)) + .add(eq("name", name)) .uniqueResult(); } diff --git a/src/main/java/org/ayfaar/app/enums/SyncType.java b/src/main/java/org/ayfaar/app/enums/SyncType.java new file mode 100644 index 00000000..7e99b945 --- /dev/null +++ b/src/main/java/org/ayfaar/app/enums/SyncType.java @@ -0,0 +1,6 @@ +package org.ayfaar.app.enums; + +public enum SyncType { + DB, + RemoteService +} diff --git a/src/main/java/org/ayfaar/app/event/DefaultRestErrorEvent.java b/src/main/java/org/ayfaar/app/event/DefaultRestErrorEvent.java new file mode 100644 index 00000000..3a19f0ea --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/DefaultRestErrorEvent.java @@ -0,0 +1,16 @@ +package org.ayfaar.app.event; + +/** + * Created by Pas8sion on 09.11.2014. + */ +public class DefaultRestErrorEvent extends PushEvent { + + + public DefaultRestErrorEvent(String title, String message) { + super(); + this.title = title; + this.message = message; + } + + +} diff --git a/src/main/java/org/ayfaar/app/event/EventPublisher.java b/src/main/java/org/ayfaar/app/event/EventPublisher.java new file mode 100644 index 00000000..bc92e7fe --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/EventPublisher.java @@ -0,0 +1,23 @@ +package org.ayfaar.app.event; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; + +@Component +/** + * Decorator for event driven methodology + */ +public class EventPublisher { + final private ApplicationEventPublisher publisher; + + @Inject + private EventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public void publishEvent(Object event) { + publisher.publishEvent(event); + } +} diff --git a/src/main/java/org/ayfaar/app/event/HasUrl.java b/src/main/java/org/ayfaar/app/event/HasUrl.java new file mode 100644 index 00000000..2cfdc8c2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/HasUrl.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.event; + +public interface HasUrl { + String getUrl(); +} diff --git a/src/main/java/org/ayfaar/app/event/LinkPushEvent.java b/src/main/java/org/ayfaar/app/event/LinkPushEvent.java new file mode 100644 index 00000000..dbc36c5c --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/LinkPushEvent.java @@ -0,0 +1,29 @@ +package org.ayfaar.app.event; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +public class LinkPushEvent extends PushEvent implements HasUrl { + protected String url; + + public LinkPushEvent(String title, String url) { + this.title = title; + this.url = getUrlToTerm(url); + } + + public LinkPushEvent() { + } + + @Override + public String getUrl() { + return url; + } + + protected static String getUrlToTerm(String term) { + try { + return BASE_URL+"/"+ URLEncoder.encode(term, "UTF-8").replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/ayfaar/app/event/NewLinkEvent.java b/src/main/java/org/ayfaar/app/event/NewLinkEvent.java new file mode 100644 index 00000000..903d924c --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/NewLinkEvent.java @@ -0,0 +1,13 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Link; +import org.springframework.util.Assert; + +public class NewLinkEvent { + public final Link link; + + public NewLinkEvent(Link link) { + Assert.notNull(link, "Link is null"); + this.link = link; + } +} diff --git a/src/main/java/org/ayfaar/app/event/NewLinkPushEvent.java b/src/main/java/org/ayfaar/app/event/NewLinkPushEvent.java new file mode 100644 index 00000000..35fa29ba --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/NewLinkPushEvent.java @@ -0,0 +1,13 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Link; + +import static java.lang.String.format; + +public class NewLinkPushEvent extends LinkPushEvent { + public NewLinkPushEvent(String term, String alias, Link link) { + super(format("Связь %s + %s", term, alias), term); + message = (link.getType() != null ? "тип: " + link.getType() + "\n" : "") + +"удалить: " + getRemoveLink(link.getLinkId()); + } +} diff --git a/src/main/java/org/ayfaar/app/event/NewQuoteLinkEvent.java b/src/main/java/org/ayfaar/app/event/NewQuoteLinkEvent.java new file mode 100644 index 00000000..6fe42ed6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/NewQuoteLinkEvent.java @@ -0,0 +1,11 @@ +package org.ayfaar.app.event; + +/** + * Created by Pas8sion on 14.11.2014. + */ +public class NewQuoteLinkEvent extends LinkPushEvent { + public NewQuoteLinkEvent(String termName, String itemNumber, String quote, Integer linkId) { + super("Связь " + termName + " + " + itemNumber, termName); + message = quote + "\nlink id: " + linkId + " " + getRemoveLink(linkId); + } +} diff --git a/src/main/java/org/ayfaar/app/event/NewTermEvent.java b/src/main/java/org/ayfaar/app/event/NewTermEvent.java new file mode 100644 index 00000000..2fdad82d --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/NewTermEvent.java @@ -0,0 +1,13 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Term; + +public class NewTermEvent extends TermPushEvent { + + public NewTermEvent(Term term) { + super(term.getName()); + title = "Новый термин: "+term.getName(); + if (term.getShortDescription() != null || term.getDescription() != null) + message = term.getShortDescription() + "\n\n" + term.getDescription(); + } +} diff --git a/src/main/java/org/ayfaar/app/event/PushEvent.java b/src/main/java/org/ayfaar/app/event/PushEvent.java new file mode 100644 index 00000000..8ecc807f --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/PushEvent.java @@ -0,0 +1,26 @@ +package org.ayfaar.app.event; + +import org.springframework.context.ApplicationEvent; + +public abstract class PushEvent extends ApplicationEvent { + public static final String BASE_URL = "http://ii.ayfaar.org"; + + protected String title; + protected String message; + + public PushEvent() { + super("ii event"); + } + + public String getTitle() { + return title; + } + + public String getMessage() { + return message; + } + + protected String getRemoveLink(Integer linkId) { + return BASE_URL+"/api/link/remove/" + linkId; + } +} diff --git a/src/main/java/org/ayfaar/app/event/QuietException.java b/src/main/java/org/ayfaar/app/event/QuietException.java new file mode 100644 index 00000000..c42b240b --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/QuietException.java @@ -0,0 +1,22 @@ +package org.ayfaar.app.event; + +public class QuietException extends RuntimeException { + public QuietException() { + } + + public QuietException(String message) { + super(message); + } + + public QuietException(String message, Throwable cause) { + super(message, cause); + } + + public QuietException(Throwable cause) { + super(cause); + } + + public QuietException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/org/ayfaar/app/event/RecordRenamedEvent.java b/src/main/java/org/ayfaar/app/event/RecordRenamedEvent.java new file mode 100644 index 00000000..d9b1bfe8 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/RecordRenamedEvent.java @@ -0,0 +1,11 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Record; + +public class RecordRenamedEvent { + public final Record record; + + public RecordRenamedEvent(Record record) { + this.record = record; + } +} diff --git a/src/main/java/org/ayfaar/app/event/SearchEvent.java b/src/main/java/org/ayfaar/app/event/SearchEvent.java new file mode 100644 index 00000000..f6946b77 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/SearchEvent.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.controllers.search.cache.SearchCacheKey; + +public class SearchEvent extends LinkPushEvent { + public SearchEvent(SearchCacheKey key) { + super("Поиск " + key.query + + (key.startFrom != null && !key.startFrom.isEmpty() ? " начиная с "+key.startFrom : "") + + (key.page > 0 ? " (страница "+(key.page+1)+")" : ""), key.query); + } + +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/event/SearchQuoteEvent.java b/src/main/java/org/ayfaar/app/event/SearchQuoteEvent.java new file mode 100644 index 00000000..dcac0aec --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/SearchQuoteEvent.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Item; +import org.ayfaar.app.model.Term; + +public class SearchQuoteEvent extends LinkPushEvent { + public SearchQuoteEvent(Term term, Item item, String quote, Integer linkId) { + super("Связь через поиск (" + term.getName() + " + " + item.getNumber() + ")", term.getName()); + message = quote+ (linkId == null ? "\n\nНе создана по причине возможной дубликации" + : "\n\nудалить связь "+getRemoveLink(linkId)); + } +} diff --git a/src/main/java/org/ayfaar/app/event/SimplePushEvent.java b/src/main/java/org/ayfaar/app/event/SimplePushEvent.java new file mode 100644 index 00000000..4d26d083 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/SimplePushEvent.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.event; + +public class SimplePushEvent extends PushEvent { + public SimplePushEvent(String title, String message) { + this.title = title; + this.message = message; + } + + public SimplePushEvent(String title) { + this.title = title; + } +} diff --git a/src/main/java/org/ayfaar/app/event/SysLogEvent.java b/src/main/java/org/ayfaar/app/event/SysLogEvent.java new file mode 100644 index 00000000..c31fb2d8 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/SysLogEvent.java @@ -0,0 +1,22 @@ +package org.ayfaar.app.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.logging.LogLevel; + +@Getter +@Setter +@Builder +public class SysLogEvent { + private String source; + private String message; + private LogLevel level; + + public SysLogEvent(Object source, String message, LogLevel level) { + this.source = source instanceof String ? (String) source : source.getClass().getSimpleName(); + this.message = message; + this.level = level; + } + +} diff --git a/src/main/java/org/ayfaar/app/event/TermAddEvent.java b/src/main/java/org/ayfaar/app/event/TermAddEvent.java new file mode 100644 index 00000000..baaecf3b --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/TermAddEvent.java @@ -0,0 +1,15 @@ +package org.ayfaar.app.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +public class TermAddEvent extends ApplicationEvent{ + + @Getter + protected String term; + + public TermAddEvent(String term) { + super("ii event"); + this.term = term; + } +} diff --git a/src/main/java/org/ayfaar/app/event/TermPushEvent.java b/src/main/java/org/ayfaar/app/event/TermPushEvent.java new file mode 100644 index 00000000..11a78548 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/TermPushEvent.java @@ -0,0 +1,14 @@ +package org.ayfaar.app.event; + + +import lombok.Getter; + +public class TermPushEvent extends LinkPushEvent{ + @Getter + protected String name; + + public TermPushEvent(String name) { + super(); + this.name = name; + } +} diff --git a/src/main/java/org/ayfaar/app/event/TermUpdatedEvent.java b/src/main/java/org/ayfaar/app/event/TermUpdatedEvent.java new file mode 100644 index 00000000..a41961d3 --- /dev/null +++ b/src/main/java/org/ayfaar/app/event/TermUpdatedEvent.java @@ -0,0 +1,24 @@ +package org.ayfaar.app.event; + +import org.ayfaar.app.model.Term; + +public class TermUpdatedEvent extends TermPushEvent { + + public String morphAlias; + + public TermUpdatedEvent(Term term, String oldShortDescription, String oldDescription) { + super(term.getName()); + title = "Обновлён термин: " +term.getName(); + message = "Предыдущий вариант:\n" + oldShortDescription +"\n\n" + oldDescription; + url = getUrlToTerm(term.getName()); + } + + public TermUpdatedEvent(Term term) { + this(term, null, null); + } + + public TermUpdatedEvent(Term term, String morphAlias) { + this(term); + this.morphAlias = morphAlias; + } +} diff --git a/src/main/java/org/ayfaar/app/model/ActionEvent.java b/src/main/java/org/ayfaar/app/model/ActionEvent.java new file mode 100644 index 00000000..24354c28 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/ActionEvent.java @@ -0,0 +1,26 @@ +package org.ayfaar.app.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.ayfaar.app.services.moderation.Action; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@Getter @Setter +@NoArgsConstructor +public class ActionEvent { + @Id + @GeneratedValue + private Integer id; + @Column(nullable = false) + private Date createdAt = new Date(); + @Column(nullable = false, columnDefinition = "text") + private String message; + private Integer createdBy; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Action action; +} diff --git a/src/main/java/org/ayfaar/app/model/Article.java b/src/main/java/org/ayfaar/app/model/Article.java index 3231a2ec..94dd0037 100644 --- a/src/main/java/org/ayfaar/app/model/Article.java +++ b/src/main/java/org/ayfaar/app/model/Article.java @@ -52,4 +52,9 @@ public String getContent() { public void setContent(String content) { this.content = content; } + + @Override + public String toTitle() { + return name; + } } diff --git a/src/main/java/org/ayfaar/app/model/Category.java b/src/main/java/org/ayfaar/app/model/Category.java index f798bde4..21712955 100644 --- a/src/main/java/org/ayfaar/app/model/Category.java +++ b/src/main/java/org/ayfaar/app/model/Category.java @@ -1,5 +1,6 @@ package org.ayfaar.app.model; +import lombok.Builder; import lombok.NoArgsConstructor; import org.ayfaar.app.annotations.Uri; @@ -14,12 +15,12 @@ @Uri(nameSpace = "категория:") public class Category extends UID { - public static final String PARAGRAPH_NAME = "Параграф"; +// public static final String PARAGRAPH_NAME = "параграф:"; public static final String TOM_NAME = "Том"; public static final String PARAGRAPH_SIGN = "§"; @Column(unique = true) - private String name; + private String name; //todo rename to code @Column(columnDefinition = "TEXT") private String description; private String parent; @@ -35,6 +36,11 @@ public Category(String name, String description, String parent) { this(name, parent); this.description = description; } + @Builder + public Category(String name, String description, String parent, String next) { + this(name, parent, description); + this.next = next; + } public Category(String name) { this.name = name; @@ -88,10 +94,10 @@ public void setDescription(String description) { this.description = description; } - public boolean isParagraph() { - return name.indexOf(PARAGRAPH_NAME) == 0; +// public boolean isParagraph() { +// return name.indexOf(PARAGRAPH_NAME) == 0; // return start != null && !start.isEmpty(); - } +// } public boolean isTom() { return name.indexOf(TOM_NAME) == 0; @@ -100,4 +106,9 @@ public boolean isTom() { public boolean isCikl() { return name.equals("БДК") || name.equals("Основы"); } + + @Override + public String toTitle() { + return name; + } } diff --git a/src/main/java/org/ayfaar/app/model/Document.java b/src/main/java/org/ayfaar/app/model/Document.java new file mode 100644 index 00000000..a767e4a8 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Document.java @@ -0,0 +1,57 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.annotations.Uri; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import java.util.Date; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "документ:google:", field = "id") +@Getter +@Setter +@NoArgsConstructor +@RequiredArgsConstructor +public class Document extends UID /*implements HasSequence*/ { + @NonNull + @Column(unique = true) + private String id; + @Column(nullable = false) + @NonNull + private String name; + @Column(columnDefinition = "text") + private String annotation; + private String author; + private String thumbnail; + private String mimeType; + private String icon; + private String downloadUrl; + private Date createdAt = new Date(); + + @Builder + public Document(String id, String name, String annotation, String author, String thumbnail, String mimeType, String icon, String downloadUrl) { + this.id = id; + this.name = name; + this.annotation = annotation; + this.author = author; + this.thumbnail = thumbnail; + this.mimeType = mimeType; + this.icon = icon; + this.downloadUrl = downloadUrl; + } + + @Override + public String toTitle() { + return name; + } + + /*@Override + public Class getSequence() { + return DocumentSeq.class; + }*/ + + +} diff --git a/src/main/java/org/ayfaar/app/model/HasSequence.java b/src/main/java/org/ayfaar/app/model/HasSequence.java new file mode 100644 index 00000000..21191508 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/HasSequence.java @@ -0,0 +1,8 @@ +package org.ayfaar.app.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public interface HasSequence { + @JsonIgnore + Class getSequence(); +} diff --git a/src/main/java/org/ayfaar/app/model/HasUri.java b/src/main/java/org/ayfaar/app/model/HasUri.java new file mode 100644 index 00000000..97317389 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/HasUri.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.model; + +public interface HasUri { + public String getUri(); +} diff --git a/src/main/java/org/ayfaar/app/model/Image.java b/src/main/java/org/ayfaar/app/model/Image.java new file mode 100644 index 00000000..3d63e518 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Image.java @@ -0,0 +1,46 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.annotations.Uri; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import java.util.Date; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "изображение:", field = "id") +@Getter +@Setter +@NoArgsConstructor +@RequiredArgsConstructor +public class Image extends UID { + + @NonNull + @Column(unique = true) + private String id; + @Column(nullable = false) + @NonNull + private String name; + private String mimeType; + private String downloadUrl; + private String thumbnail; + private Date createdAt = new Date(); + @Column(columnDefinition = "text") + private String comment; + + @Builder + public Image(String id, String name, String mimeType, String downloadUrl, String thumbnail) { + this.id = id; + this.name = name; + this.mimeType = mimeType; + this.downloadUrl = downloadUrl; + this.thumbnail = thumbnail; + } + + @Override + public String toTitle() { + return name; + } +} diff --git a/src/main/java/org/ayfaar/app/model/Item.java b/src/main/java/org/ayfaar/app/model/Item.java index 54024423..c8dac9b3 100644 --- a/src/main/java/org/ayfaar/app/model/Item.java +++ b/src/main/java/org/ayfaar/app/model/Item.java @@ -1,32 +1,41 @@ package org.ayfaar.app.model; +import lombok.Getter; +import lombok.Setter; import org.ayfaar.app.annotations.Uri; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.PrimaryKeyJoinColumn; +import static java.lang.Float.parseFloat; + @Entity @PrimaryKeyJoinColumn(name="uri") -//@Audited +@Setter @Getter @Uri(nameSpace = "ии:пункт:", field = "number") -public class Item extends UID implements Comparable{ +public class Item extends UID { - @Column(unique = true) + @Column(unique = true, nullable = false) private String number; @Column(columnDefinition = "TEXT") private String content; @Column(columnDefinition = "TEXT") - private String wiki; + private String taggedContent; +// @Column(columnDefinition = "TEXT") +// private String wiki; private String next; + // field for optimization order operation on database + private Float orderIndex; public Item(String number, String content) { - this.number = number; + this(number); this.content = content; } public Item(String number) { this.number = number; + orderIndex = parseFloat(number); } public Item() { @@ -36,44 +45,15 @@ public static boolean isItemNumber(String s) { return s.matches("^\\d\\d?\\.\\d{4}\\d?$"); } - public String getNumber() { + @Override + public String toTitle() { return number; } - public void setNumber(String number) { - this.number = number; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public String getNext() { - return next; - } - - public void setNext(String next) { - this.next = next; - } - - public String getWiki() { - return wiki; - } - - public void setWiki(String wiki) { - this.wiki = wiki; - } - - @Override - public int compareTo(Item that) { - double itemNumber1 = Double.parseDouble(this.getNumber()); - double itemNumber2 = Double.parseDouble(that.getNumber()); - - return (itemNumber1 == itemNumber2) ? 0 : (itemNumber1 > itemNumber2) ? 1 : -1; - - } + /* + order index sql: + ALTER TABLE `item` ADD COLUMN `order_index` DECIMAL(10,5) NULL DEFAULT NULL AFTER `next`; + update item set order_index = cast(number as decimal(10, 5)); + ALTER TABLE `item` ADD INDEX `order_index` (`order_index`); + */ } diff --git a/src/main/java/org/ayfaar/app/model/ItemsRange.java b/src/main/java/org/ayfaar/app/model/ItemsRange.java new file mode 100644 index 00000000..ddd64b53 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/ItemsRange.java @@ -0,0 +1,55 @@ +package org.ayfaar.app.model; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.ayfaar.app.annotations.Uri; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import java.util.Date; + +import static org.ayfaar.app.model.ItemsRange.NAME_SPACE; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@NoArgsConstructor +@Uri(nameSpace = NAME_SPACE) +@Data +@EqualsAndHashCode(callSuper = true) +public class ItemsRange extends UID { + public static final String NAME_SPACE = "ии:пункты:"; + public static final Class SEQUENCE = ItemsRangeSeq.class; +// from: 5.0001 +// to: 5.0002 (определил по следующей строке) +// code: 5.17.1.1 (том.раздел.глава.параграф) +// description: "Ииссиидиология не признаётся наукой, которая в свою очередь не может ответить на вопросы о структуре Самосознания. Поэтому представления людей о "своей душе" туманны и надуманы." +// uri: ии:пункты:5.17.1.1 + @Column(name = "`from`") + private String from; + @Column(name = "`to`") + private String to; + @Column(unique = true) + private String code; + @Column(columnDefinition = "TEXT") + private String description; + private String category; // uri of parent category + private Date createdAt = new Date(); + + @Builder + public ItemsRange(String from, String to, String code, String description, String category) { + this.from = from; + this.to = to; + this.code = code; + this.description = description; + this.category = category; + if (this.code == null) this.code = this.from + "-" + this.to; + } + + @Override + public String toTitle() { + return description != null && !description.isEmpty() ? description : code; + } +} diff --git a/src/main/java/org/ayfaar/app/model/ItemsRangeSeq.java b/src/main/java/org/ayfaar/app/model/ItemsRangeSeq.java new file mode 100644 index 00000000..c27772da --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/ItemsRangeSeq.java @@ -0,0 +1,18 @@ +package org.ayfaar.app.model; + +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * Created by RuAV on 16.12.2015. + */ +@Entity +@Data +public class ItemsRangeSeq { + @Id + @GeneratedValue + private Integer seq; +} diff --git a/src/main/java/org/ayfaar/app/model/LightLink.java b/src/main/java/org/ayfaar/app/model/LightLink.java new file mode 100644 index 00000000..57b0e83e --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/LightLink.java @@ -0,0 +1,52 @@ +package org.ayfaar.app.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; + +import javax.persistence.*; +import java.util.Date; + +import static org.ayfaar.app.utils.hibernate.EnumHibernateType.CLASS; +import static org.ayfaar.app.utils.hibernate.EnumHibernateType.ENUM; + +@Entity +@Data +@NoArgsConstructor +public class LightLink { + + @Id + @GeneratedValue + private Integer linkId; + @Type(type = ENUM, parameters = @org.hibernate.annotations.Parameter(name = CLASS, value = "org.ayfaar.app.model.LinkType")) + private LinkType type; + private String source; + @Column(columnDefinition = "TEXT") + private String quote; + @Column(columnDefinition = "TEXT") + private String taggedQuote; + @Column(columnDefinition = "TEXT") + private String comment; + private Float rate; + private Date createdAt = new Date(); + private Integer createdBy; + + private String uid1; + private String uid2; + + public static LightLink fromLink(Link l) { + final LightLink ll = new LightLink(); + ll.setLinkId(l.getLinkId()); + ll.setType(l.getType()); + ll.setSource(l.getSource()); + ll.setQuote(l.getQuote()); + ll.setTaggedQuote(l.getTaggedQuote()); + ll.setComment(l.getComment()); + ll.setRate(l.getRate()); + ll.setCreatedAt(l.getCreatedAt()); + ll.setCreatedBy(l.getCreatedBy()); + ll.setUid1(l.getUid1().getUri()); + ll.setUid2(l.getUid2().getUri()); + return ll; + } +} diff --git a/src/main/java/org/ayfaar/app/model/Link.java b/src/main/java/org/ayfaar/app/model/Link.java index e913805d..09b69d6e 100644 --- a/src/main/java/org/ayfaar/app/model/Link.java +++ b/src/main/java/org/ayfaar/app/model/Link.java @@ -1,37 +1,39 @@ package org.ayfaar.app.model; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; import javax.persistence.*; +import java.util.Date; + +import static org.ayfaar.app.utils.hibernate.EnumHibernateType.CLASS; +import static org.ayfaar.app.utils.hibernate.EnumHibernateType.ENUM; @Entity @Data @NoArgsConstructor +/** + * Связывает между собой две произвольные сущности наследуюющие UID + */ public class Link { - /** - * Синомим, первым следует указывать более точное понятие или код - */ - public static final Byte ALIAS = 1; - - /** - * Аббревиатура или сокращение, первым указывают полное значение - */ - public static final Byte ABBREVIATION = 2; - - /** - * Ссылка на код понятия - * Первый понятие, воторой код - */ - public static final Byte CODE = 4; @Id @GeneratedValue private Integer linkId; - private Byte type; + @Type(type = ENUM, parameters = @org.hibernate.annotations.Parameter(name = CLASS, value = "org.ayfaar.app.model.LinkType")) + private LinkType type; private String source; @Column(columnDefinition = "TEXT") private String quote; + @Column(columnDefinition = "TEXT") + private String taggedQuote; + @Column(columnDefinition = "TEXT") + private String comment; + private Float rate; + private Date createdAt = new Date(); + private Integer createdBy; @ManyToOne private UID uid1; @@ -48,13 +50,17 @@ public Link(UID uid1, UID uid2) { } public Link(UID uid1, UID uid2, Byte type) { + this(uid1, uid2, type == null ? null : LinkType.getEnum(type)); + } + + public Link(UID uid1, UID uid2, LinkType type) { this(uid1, uid2); this.type = type; } - public Link(Term term, Item item, String quote, String source) { + public Link(Term term, Item item, String quote, String taggedQuote) { this(term, item, quote); - this.source = source; + this.taggedQuote = taggedQuote; } public Link(UID uid1, UID uid2, String quote) { @@ -62,4 +68,20 @@ public Link(UID uid1, UID uid2, String quote) { this.uid2 = uid2; this.quote = quote; } + + public Link(UID uid1, UID uid2, LinkType type, String comment) { + this(uid1, uid2, type); + this.comment = comment; + } + + @Builder + public Link(UID uid1, UID uid2, LinkType type, String comment, String quote, Float rate, Integer createdBy) { + this.quote = quote; + this.rate = rate; + this.uid1 = uid1; + this.uid2 = uid2; + this.type = type; + this.comment = comment; + this.createdBy = createdBy; + } } diff --git a/src/main/java/org/ayfaar/app/model/LinkType.java b/src/main/java/org/ayfaar/app/model/LinkType.java new file mode 100644 index 00000000..c96734af --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/LinkType.java @@ -0,0 +1,54 @@ +package org.ayfaar.app.model; + +import org.ayfaar.app.utils.hibernate.ValueEnum; + +public enum LinkType implements ValueEnum { + /** + * Синомим, первым следует указывать более точное понятие или код + */ + ALIAS(1), + /** + * Аббревиатура или сокращение, первым указывают полное значение + */ + ABBREVIATION(2), + /** + * Ссылка на код понятия + * Первый понятие, второй код + */ + CODE(4), + /** + * Ссылка на дочерний объет + * Первый родитель, второй потомок + */ + CHILD(5), + /** + * Перевод + */ + TRANSLATION(6); + + protected Byte value; + + LinkType(int value) { + this.value = (byte) value; + } + + @Override + public Byte getValue() { + return value; + } + + public static LinkType getEnum(String value) { + return getEnum(Byte.valueOf(value)); + } + + public static LinkType getEnum(Byte value) { + for (LinkType status : values()) { + if (status.getValue().equals(value)) return status; + } + throw new RuntimeException("No LinkType for "+value); + } + + public boolean isChild() { + return this == CHILD; + } +} diff --git a/src/main/java/org/ayfaar/app/model/PendingAction.java b/src/main/java/org/ayfaar/app/model/PendingAction.java new file mode 100644 index 00000000..a2fb88e0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/PendingAction.java @@ -0,0 +1,29 @@ +package org.ayfaar.app.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.ayfaar.app.services.moderation.Action; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class PendingAction { + @Id + @GeneratedValue + private Integer id; + @Column(nullable = false, columnDefinition = "text") + private String message; + private Integer initiatedBy; + @Column(columnDefinition = "text", nullable = false) + private String command; + private Date createdAt = new Date(); + @Enumerated(EnumType.STRING) + private Action action; + private Date confirmedAt; + private Integer confirmedBy; +} diff --git a/src/main/java/org/ayfaar/app/model/Record.java b/src/main/java/org/ayfaar/app/model/Record.java new file mode 100644 index 00000000..694a782a --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Record.java @@ -0,0 +1,44 @@ +package org.ayfaar.app.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.ayfaar.app.annotations.Uri; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import java.util.Date; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "запись:", field = "code") +public class Record extends UID{ + + private String code; + @Column(columnDefinition = "text") + private String name; + private String previousName; + private Date recorderAt; + private Date createdAt; + private String audioUrl; + private String altAudioGid; + @Column(columnDefinition = "text") + private String text; + private Integer duration = 0; + @Column(columnDefinition = "text") + private String description; + + @Override + public String toTitle() { + return name; + } + + public enum Kind { + k, // коллоквиум + b // беседа + } +} diff --git a/src/main/java/org/ayfaar/app/model/Resource.java b/src/main/java/org/ayfaar/app/model/Resource.java new file mode 100644 index 00000000..2732323c --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Resource.java @@ -0,0 +1,39 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.annotations.Uri; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.PrimaryKeyJoinColumn; +import java.util.Date; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "url:", field = "uri") +@Getter @Setter +@NoArgsConstructor +@RequiredArgsConstructor +public class Resource extends UID { + @Column(nullable = false) + @NonNull + private String title; + + @NonNull + @Column(nullable = false) + private ResourceType type; + + @Column(columnDefinition = "TEXT") + private String comments; + + private Date createdAt = new Date(); + + @Override + public String toTitle() { + return title; + } + + public String getUrl() { + return uri.replaceFirst("url:", ""); + } +} diff --git a/src/main/java/org/ayfaar/app/model/ResourceType.java b/src/main/java/org/ayfaar/app/model/ResourceType.java new file mode 100644 index 00000000..dc8af3a0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/ResourceType.java @@ -0,0 +1,22 @@ +package org.ayfaar.app.model; + +import org.ayfaar.app.utils.hibernate.ValueEnum; + +public enum ResourceType implements ValueEnum { + article('A'), video('V'); + + private final char code; + + ResourceType(char code) { + this.code = code; + } + + @Override + public Character getValue() { + return code; + } + + public boolean isVideo() { + return this == video; + } +} diff --git a/src/main/java/org/ayfaar/app/model/Revision.java b/src/main/java/org/ayfaar/app/model/Revision.java deleted file mode 100644 index 65dfafd0..00000000 --- a/src/main/java/org/ayfaar/app/model/Revision.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.ayfaar.app.model; - -import org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity; - -//@Entity -//@RevisionEntity -//@Table -public class Revision extends DefaultTrackingModifiedEntitiesRevisionEntity { - private boolean wikiSynchronized; - - public boolean isWikiSynchronized() { - return wikiSynchronized; - } - - public void setWikiSynchronized(boolean wikiSynchronized) { - this.wikiSynchronized = wikiSynchronized; - } -} diff --git a/src/main/java/org/ayfaar/app/model/Sequence.java b/src/main/java/org/ayfaar/app/model/Sequence.java new file mode 100644 index 00000000..effa19d4 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Sequence.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.model; + +public interface Sequence { + Integer getSeq(); +} diff --git a/src/main/java/org/ayfaar/app/model/Song.java b/src/main/java/org/ayfaar/app/model/Song.java deleted file mode 100644 index a5b48cfe..00000000 --- a/src/main/java/org/ayfaar/app/model/Song.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.ayfaar.app.model; -import lombok.Data; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; - -@Entity -@Data -public class Song { - - @Id - @GeneratedValue - private Integer id; - private String name; - private String info; - @Column(columnDefinition = "TEXT") - private String content; - - public Song(Integer id, String name, String info, String content) { - this.id = id; - this.name = name; - this.info = info; - this.content = content; - } - -} diff --git a/src/main/java/org/ayfaar/app/model/SyncStatus.java b/src/main/java/org/ayfaar/app/model/SyncStatus.java deleted file mode 100644 index 9de54f0c..00000000 --- a/src/main/java/org/ayfaar/app/model/SyncStatus.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.ayfaar.app.model; - -import lombok.Data; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import java.util.Date; - -@Entity -@Data -public class SyncStatus { - @Id @GeneratedValue - private Integer id; - @Column(unique = true, nullable = false) - private String articleName; - @Column(columnDefinition = "LONGTEXT", nullable = false) - private String articleContent; - private Boolean synchronised; - private Date syncDate; -} diff --git a/src/main/java/org/ayfaar/app/model/SysLog._java b/src/main/java/org/ayfaar/app/model/SysLog._java new file mode 100644 index 00000000..e48b003a --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/SysLog._java @@ -0,0 +1,35 @@ +/*package org.ayfaar.app.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.boot.logging.LogLevel; + +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import java.util.Date; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class SysLog { + @Id + @GeneratedValue + private Integer id; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + private Date date; + private String logger; + @Lob + private String message; + @Enumerated(EnumType.STRING) + private LogLevel level; +}*/ diff --git a/src/main/java/org/ayfaar/app/model/Term.java b/src/main/java/org/ayfaar/app/model/Term.java index 4b2b60e2..ca401e79 100644 --- a/src/main/java/org/ayfaar/app/model/Term.java +++ b/src/main/java/org/ayfaar/app/model/Term.java @@ -1,5 +1,7 @@ package org.ayfaar.app.model; +import lombok.Getter; +import lombok.Setter; import org.ayfaar.app.annotations.Uri; import javax.persistence.Column; @@ -10,13 +12,19 @@ //@Audited @PrimaryKeyJoinColumn(name="uri") @Uri(nameSpace = "ии:термин:") +@Getter @Setter public class Term extends UID { @Column(unique = true) private String name; + @Column(columnDefinition = "TEXT") private String shortDescription; @Column(columnDefinition = "TEXT") + private String taggedShortDescription; + @Column(columnDefinition = "TEXT") private String description; + @Column(columnDefinition = "TEXT") + private String taggedDescription; public Term(String name) { @@ -36,28 +44,8 @@ public Term(String name, String shortDescription, String description) { this.shortDescription = shortDescription; } - - public String getName() { + @Override + public String toTitle() { return name; } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getShortDescription() { - return shortDescription; - } - - public void setShortDescription(String shortDescription) { - this.shortDescription = shortDescription; - } } diff --git a/src/main/java/org/ayfaar/app/model/TermParagraph.java b/src/main/java/org/ayfaar/app/model/TermParagraph.java new file mode 100644 index 00000000..83688cf7 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/TermParagraph.java @@ -0,0 +1,23 @@ +package org.ayfaar.app.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@IdClass(TermParagraphKey.class) +public class TermParagraph { + @Id + private String term; + @Id + private String paragraph; + +} diff --git a/src/main/java/org/ayfaar/app/model/TermParagraphKey.java b/src/main/java/org/ayfaar/app/model/TermParagraphKey.java new file mode 100644 index 00000000..170c8a59 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/TermParagraphKey.java @@ -0,0 +1,17 @@ +package org.ayfaar.app.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import java.io.Serializable; + +@Getter +@Setter +@EqualsAndHashCode +@ToString +public class TermParagraphKey implements Serializable{ + + private String term; + private String paragraph; +} diff --git a/src/main/java/org/ayfaar/app/model/Topic.java b/src/main/java/org/ayfaar/app/model/Topic.java new file mode 100644 index 00000000..c93ca9a6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Topic.java @@ -0,0 +1,35 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.annotations.Uri; +import org.ayfaar.app.utils.Language; + +import javax.persistence.*; +import java.util.Date; + +import static org.ayfaar.app.utils.Language.ru; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "тема:") +@Getter +@Setter +@NoArgsConstructor +@RequiredArgsConstructor +/** + * Сущность хранящая название некой произвольной темы + */ +public class Topic extends UID { + @Column(unique = true) + @NonNull + private String name; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Language lang = ru; + private Date createdAt = new Date(); + + @Override + public String toTitle() { + return name; + } +} diff --git a/src/main/java/org/ayfaar/app/model/Translation.java b/src/main/java/org/ayfaar/app/model/Translation.java new file mode 100644 index 00000000..41bdd33d --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/Translation.java @@ -0,0 +1,30 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.utils.Language; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@RequiredArgsConstructor +public class Translation { + @Id + @NonNull + private String origin; + @Column(nullable = false) + @NonNull + private String translated; + @NonNull + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Language lang; + @Enumerated(EnumType.STRING) + private Context context = Context.topic; + + public enum Context { + topic + } +} diff --git a/src/main/java/org/ayfaar/app/model/UID.java b/src/main/java/org/ayfaar/app/model/UID.java index 1889c1df..40e568cf 100644 --- a/src/main/java/org/ayfaar/app/model/UID.java +++ b/src/main/java/org/ayfaar/app/model/UID.java @@ -7,12 +7,12 @@ @Entity //@Audited @Inheritance(strategy = InheritanceType.JOINED) -public abstract class UID { +public abstract class UID implements HasUri { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "UriGenerator") @GenericGenerator(name = "UriGenerator", strategy = "org.ayfaar.app.utils.UriGenerator") - private String uri; + protected String uri; @Override public boolean equals(Object o) { @@ -34,4 +34,6 @@ public String getUri() { public void setUri(String uri) { this.uri = uri; } + + abstract public String toTitle(); } diff --git a/src/main/java/org/ayfaar/app/model/User.java b/src/main/java/org/ayfaar/app/model/User.java new file mode 100644 index 00000000..295b1387 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/User.java @@ -0,0 +1,53 @@ +package org.ayfaar.app.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.ayfaar.app.controllers.OAuthProvider; +import org.ayfaar.app.services.moderation.UserRole; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@Getter @Setter +@NoArgsConstructor +public class User { + @Id + @GeneratedValue + private Integer id; + @Column(nullable = false, unique = true) + private String email; + @Column(nullable = false, unique = true) + private String accessToken; + private String firstName; + private String lastName; + @Column(nullable = false, unique = true) + private String name; + private String picture; + private String timezone; + private String thumbnail; + @Enumerated(EnumType.STRING) + private OAuthProvider oauthProvider; + @Enumerated(EnumType.STRING) + private UserRole role = UserRole.ROLE_AUTHENTICATED; + private Date createdAt = new Date(); + private Date lastVisitAt; + private Long providerId; + private Integer hiddenActionEventId; + + @Builder + public User(String email, String accessToken, String firstName, String lastName, String name, String picture, String thumbnail, String timezone, Long providerId, OAuthProvider oauthProvider) { + this.email = email; + this.accessToken = accessToken; + this.firstName = firstName; + this.lastName = lastName; + this.name = name; + this.picture = picture; + this.thumbnail = thumbnail; + this.timezone = timezone; + this.providerId = providerId; + this.oauthProvider = oauthProvider; + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/model/VideoResource.java b/src/main/java/org/ayfaar/app/model/VideoResource.java new file mode 100644 index 00000000..d9cc9c63 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/VideoResource.java @@ -0,0 +1,43 @@ +package org.ayfaar.app.model; + +import lombok.*; +import org.ayfaar.app.annotations.Uri; +import org.ayfaar.app.utils.AdvanceComparator; +import org.ayfaar.app.utils.Language; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@PrimaryKeyJoinColumn(name="uri") +@Uri(nameSpace = "видео:youtube:", field = "id") +@Getter @Setter +@NoArgsConstructor +@RequiredArgsConstructor +public class VideoResource extends UID implements Comparable { + @Column(unique = true, nullable = false) + @NonNull + private String id; + private String title; + + @NonNull + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Language lang; + + private String code; + private Date publishedAt = new Date(); + private Date createdAt = new Date(); + private Integer createdBy; + private Boolean official; + + @Override + public String toTitle() { + return title; + } + + @Override + public int compareTo(VideoResource o) { + return AdvanceComparator.INSTANCE.compare(title, o.title); + } +} diff --git a/src/main/java/org/ayfaar/app/model/package-info.java b/src/main/java/org/ayfaar/app/model/package-info.java new file mode 100644 index 00000000..f38ed979 --- /dev/null +++ b/src/main/java/org/ayfaar/app/model/package-info.java @@ -0,0 +1,11 @@ +@TypeDefs( + @TypeDef( + name = EnumHibernateType.ENUM, + typeClass = EnumHibernateType.class + ) +) +package org.ayfaar.app.model; + +import org.ayfaar.app.utils.hibernate.EnumHibernateType; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/repositories/LinkRepository.java b/src/main/java/org/ayfaar/app/repositories/LinkRepository.java new file mode 100644 index 00000000..97948aad --- /dev/null +++ b/src/main/java/org/ayfaar/app/repositories/LinkRepository.java @@ -0,0 +1,9 @@ +package org.ayfaar.app.repositories; + +import org.ayfaar.app.model.Link; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LinkRepository extends JpaRepository { +} diff --git a/src/main/java/org/ayfaar/app/repositories/TopicRepository.java b/src/main/java/org/ayfaar/app/repositories/TopicRepository.java new file mode 100644 index 00000000..7d77c887 --- /dev/null +++ b/src/main/java/org/ayfaar/app/repositories/TopicRepository.java @@ -0,0 +1,9 @@ +package org.ayfaar.app.repositories; + +import org.ayfaar.app.model.Topic; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TopicRepository extends JpaRepository { +} diff --git a/src/main/java/org/ayfaar/app/repositories/VideoResourceRepository.java b/src/main/java/org/ayfaar/app/repositories/VideoResourceRepository.java new file mode 100644 index 00000000..ca241739 --- /dev/null +++ b/src/main/java/org/ayfaar/app/repositories/VideoResourceRepository.java @@ -0,0 +1,9 @@ +package org.ayfaar.app.repositories; + +import org.ayfaar.app.model.VideoResource; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VideoResourceRepository extends JpaRepository { +} diff --git a/src/main/java/org/ayfaar/app/services/EntityLoader.java b/src/main/java/org/ayfaar/app/services/EntityLoader.java new file mode 100644 index 00000000..8c5b7de1 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/EntityLoader.java @@ -0,0 +1,31 @@ +package org.ayfaar.app.services; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.utils.SoftCache; +import org.ayfaar.app.utils.UriGenerator; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; + +@Service +public class EntityLoader { + private final CommonDao commonDao; + + private SoftCache cache = new SoftCache<>(); + + @Inject + public EntityLoader(CommonDao commonDao) { + this.commonDao = commonDao; + } + + public E get(String uri) { + //noinspection unchecked + return (E) cache.getOrCreate(uri, () -> commonDao.getOpt(UriGenerator.getClassByUri(uri), uri) + .orElseThrow(() -> new RuntimeException("Entity not found, uri: " + uri))); + } + + public void clear() { + cache.clear(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/GoogleSpreadsheetService.java b/src/main/java/org/ayfaar/app/services/GoogleSpreadsheetService.java new file mode 100644 index 00000000..99bd016e --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/GoogleSpreadsheetService.java @@ -0,0 +1,80 @@ +package org.ayfaar.app.services; + +import com.google.api.services.sheets.v4.Sheets; +import com.google.api.services.sheets.v4.model.*; +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.utils.GoogleService; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class GoogleSpreadsheetService { + private static final String VALUE_INPUT_OPTION = "RAW"; + + public List> read(String spreadsheetId, String range) throws IOException { + Sheets service = GoogleService.getSheetsService(); + ValueRange response = service.spreadsheets().values().get(spreadsheetId, range).execute(); + List> values = response.getValues(); + if (values == null) { + values = new ArrayList<>(); + } + if (values.size() == 0) { + log.debug("No data read from spreadsheet {}, range {}", spreadsheetId, range); + } + + return values; + } + + public Integer write(String spreadsheetId, Map> batchData) throws IOException { + Sheets service = GoogleService.getSheetsService(); + List valueRanges = new ArrayList<>(); + batchData.forEach((k, v) -> { + ValueRange valueRange = new ValueRange(); + valueRange.setRange("A" + k); + List> data = new ArrayList<>(); + data.add(v); + valueRange.setValues(data); + valueRanges.add(valueRange); + }); + + BatchUpdateValuesRequest request = new BatchUpdateValuesRequest(); + request.setValueInputOption(VALUE_INPUT_OPTION); + request.setData(valueRanges); + BatchUpdateValuesResponse response = service.spreadsheets().values().batchUpdate(spreadsheetId, request).execute(); + + Integer updatedRows = response.getTotalUpdatedRows(); + if (updatedRows == null) { + updatedRows = 0; + } + return updatedRows; + } + + public Integer write(String spreadsheetId, int index, List data) throws IOException { + Sheets service = GoogleService.getSheetsService(); + + ValueRange valueRange = new ValueRange(); + valueRange.setValues(Collections.singletonList(data)); + + UpdateValuesResponse response = service.spreadsheets().values() + .update(spreadsheetId, "A" + index, valueRange) + .setValueInputOption(VALUE_INPUT_OPTION) + .execute(); + + Integer updatedRows = response.getUpdatedRows(); + if (updatedRows == null) { + updatedRows = 0; + } + return updatedRows; + } + + public void clear(String spreadsheetId, String range) throws IOException { + Sheets service = GoogleService.getSheetsService(); + service.spreadsheets().values().clear(spreadsheetId, range, new ClearValuesRequest()).execute(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/ItemService.java b/src/main/java/org/ayfaar/app/services/ItemService.java new file mode 100644 index 00000000..398d0c63 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/ItemService.java @@ -0,0 +1,34 @@ +package org.ayfaar.app.services; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.utils.UriGenerator; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Map; +import java.util.function.Function; + +@Service +public class ItemService { + private final ItemDao itemDao; + private Map allUriNumbers; + + @Inject + public ItemService(ItemDao itemDao) { + this.itemDao = itemDao; + } + + @PostConstruct + private void init() { + allUriNumbers = StreamEx.of(itemDao.getAllNumbers()) + .sorted() + .toSortedMap(n -> UriGenerator.generate(Item.class, n), Function.identity()); + } + + public Map getAllUriNumbers() { + return allUriNumbers; + } +} diff --git a/src/main/java/org/ayfaar/app/services/TranslationService.java b/src/main/java/org/ayfaar/app/services/TranslationService.java new file mode 100644 index 00000000..3366066e --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/TranslationService.java @@ -0,0 +1,126 @@ +package org.ayfaar.app.services; + +import one.util.streamex.StreamEx; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Translation; +import org.ayfaar.app.utils.Language; +import org.ayfaar.app.utils.StringUtils; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static java.util.regex.Pattern.*; + +//@Service +public class TranslationService { + private final String MARK_IN = "‹"; + private final String MARK_OUT = "›"; + private final List endings; + private CommonDao commonDao; + + private Map>> map = new LinkedHashMap<>(); + + @Inject + public TranslationService(CommonDao commonDao, ResourceLoader resourceLoader) throws IOException { + this.commonDao = commonDao; + final Resource endingsResource = resourceLoader.getResource(ResourceLoader.CLASSPATH_URL_PREFIX + "translation/endings.txt"); + endings = FileUtils.readLines(endingsResource.getFile()); + endings.sort((o1, o2) -> Integer.compare(o2.length(), o1.length())); + load(); + } + + private void load() { + StreamEx.of(commonDao.getAll(Translation.class)) + .groupingBy(Translation::getLang) + .forEach((language, translations) -> { + final List> list = StreamEx.of(translations) + .map(t -> Pair.of(t.getOrigin(), t.getTranslated())) + .sorted((o1, o2) -> Integer.compare(o2.getKey().length(), o1.getKey().length())) + .toList(); + this.map.put(language, list); + }); + } + + public String translate(String content, Language language) { + if (content == null || content.isEmpty()) return content; + + StringBuilder result = new StringBuilder(content); + + String _endings = org.apache.commons.lang3.StringUtils.join(endings, "|"); + + for (Pair entry : map.get(language)) { + // получаем слово связаное с термином, напрмер "времени" будет связано с термином "Время" + String word = entry.getKey(); + // составляем условие по которому проверяем есть ли это слов в тексте + //Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|\\-])|^)(" + word + Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|])|^)(" + word +")(" + _endings + ")?", UNICODE_CHARACTER_CLASS | UNICODE_CASE | CASE_INSENSITIVE); + Matcher contentMatcher = pattern.matcher(content); + // если есть: + if (contentMatcher.find()) { + // ищем в результирующем тексте + Matcher matcher = pattern.matcher(result); + int offset = 0; + // if (matcher.find()) { + //перенесем обрамления для каждого слова - одно слово может встречаться несколько раз с разными обрамл. + while (offset < result.length() && matcher.find(offset)) { + offset = matcher.end(); + // сохраняем найденое слово из текста так как оно может быть в разных регистрах, + // например с большой буквы, или полностью большими буквами + if (wordInTranslated(result.substring(0, matcher.start()))) { + continue; + } + + String charBefore = matcher.group(2) != null ? matcher.group(2) : ""; + if (charBefore.equals(MARK_IN)) continue; + + String foundWord = matcher.group(3); + String ending = matcher.group(4) != null ? matcher.group(4) : ""; + final String translated = changeCase(foundWord, entry.getValue()); + String replacement = format("%s"+MARK_IN+"%s%s"+MARK_OUT+"%s", + charBefore, + foundWord, + ending, + translated + ); + result.replace(matcher.start(), matcher.end(), replacement); + //увеличим смещение с учетом замены + offset = matcher.start() + replacement.length(); + // убираем обработанный термин, чтобы не заменить его более мелким + content = contentMatcher.replaceAll(" "); + } + } + } + return result.toString(); + } + + private String changeCase(String foundWord, String translation) { + if (match("^[А-ЯЁа-яё]+$", foundWord)) { + if (match("^[А-ЯЁ][а-яё]+$", foundWord)) return StringUtils.firstUpper(translation); + if (match("^[А-ЯЁ]+$", foundWord)) return translation.toUpperCase(); + if (match("^[а-яё]+$", foundWord)) return translation.toLowerCase(); + } + return translation; + } + + private boolean match(String regexp, String word) { + return Pattern.compile(regexp, UNICODE_CHARACTER_CLASS | UNICODE_CASE).matcher(word).matches(); + } + + private boolean wordInTranslated(String substring) { + int startTag = substring.lastIndexOf(MARK_IN); + int endTag = substring.lastIndexOf(MARK_OUT); + + return startTag >= 0 && startTag > endTag; + } +} diff --git a/src/main/java/org/ayfaar/app/services/document/DocumentService.java b/src/main/java/org/ayfaar/app/services/document/DocumentService.java new file mode 100644 index 00000000..d3dc10f0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/document/DocumentService.java @@ -0,0 +1,8 @@ +package org.ayfaar.app.services.document; + +import java.util.Map; + +public interface DocumentService { + + Map getAllUriNames(); +} diff --git a/src/main/java/org/ayfaar/app/services/document/DocumentServiceImpl.java b/src/main/java/org/ayfaar/app/services/document/DocumentServiceImpl.java new file mode 100644 index 00000000..a1ed8a7d --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/document/DocumentServiceImpl.java @@ -0,0 +1,18 @@ +package org.ayfaar.app.services.document; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.Map; +import java.util.stream.Collectors; + +@Component() +public class DocumentServiceImpl implements DocumentService { + + @Autowired CommonDao commonDao; + @Override + public Map getAllUriNames(){ + return commonDao.getAll(Document.class).stream().collect(Collectors.toMap(document -> document.getUri(),document -> document.getName())); + } +} diff --git a/src/main/java/org/ayfaar/app/services/images/ImageService.java b/src/main/java/org/ayfaar/app/services/images/ImageService.java new file mode 100644 index 00000000..42d8e55b --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/images/ImageService.java @@ -0,0 +1,23 @@ +package org.ayfaar.app.services.images; + +import org.ayfaar.app.model.Image; + +import java.util.List; +import java.util.Map; + +public interface ImageService { + + void reload(); + + List getAllImages(); + + Map getAllUriNames(); + + void registerImage(Image image); + + void removeImage(Image image); + + Map getImagesKeywords(); + + Image getByUri(String uri); +} diff --git a/src/main/java/org/ayfaar/app/services/images/ImageServiceImpl.java b/src/main/java/org/ayfaar/app/services/images/ImageServiceImpl.java new file mode 100644 index 00000000..b6e01dd0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/images/ImageServiceImpl.java @@ -0,0 +1,76 @@ +package org.ayfaar.app.services.images; + +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Image; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.services.topics.TopicService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class ImageServiceImpl implements ImageService { + private final CommonDao commonDao; + private final TopicService topicService; + private List allImages; + + @Autowired + public ImageServiceImpl(CommonDao commonDao, TopicService topicService) { + this.commonDao = commonDao; + this.topicService = topicService; + } + + @PostConstruct + private void init() { + log.info("Images loading..."); + + allImages = commonDao.getAll(Image.class); + + log.info("Images loaded"); + } + + @Override + public void reload() { + init(); + } + + @Override + public List getAllImages(){ + return allImages; + } + + @Override + public Map getAllUriNames(){ + return allImages.stream().collect(Collectors.toMap(UID::getUri, Image::getName)); + } + + @Override + public void registerImage(Image image) { + allImages.add(image); + } + + @Override + public void removeImage(Image image) { + allImages.removeIf(i -> Objects.equals(i.getId(), image.getId())); + } + + @Override + public Map getImagesKeywords(){ + return StreamEx.of(allImages).toMap(UID::getUri, image -> topicService.getAllLinkedWith(image.getUri()) + .map(tp -> tp.topic().getName()) + .joining(", ")); + } + + @Override + public Image getByUri(String uri){ + return allImages.stream().filter(image -> image.getUri() == uri).findFirst().get(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeService.java b/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeService.java new file mode 100644 index 00000000..b017898c --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeService.java @@ -0,0 +1,15 @@ +package org.ayfaar.app.services.itemRange; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.model.ItemsRange; + +import java.util.List; + +public interface ItemRangeService { + + void reload(); + + List getWithCategories(); + + StreamEx getParagraphsByMainTerm(String term); +} diff --git a/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeServiceImpl.java b/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeServiceImpl.java new file mode 100644 index 00000000..575b7002 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/itemRange/ItemRangeServiceImpl.java @@ -0,0 +1,59 @@ +package org.ayfaar.app.services.itemRange; + +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.dao.ItemsRangeDao; +import org.ayfaar.app.model.ItemsRange; +import org.ayfaar.app.model.TermParagraph; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Slf4j +@Service +public class ItemRangeServiceImpl implements ItemRangeService{ + + @Autowired CommonDao commonDao; + @Autowired ItemsRangeDao itemsRangeDao; + + private List allTermParagraph; + private List itemsRanges; + + + + @PostConstruct + private void init() { + log.info("TermParagraph loading..."); + + allTermParagraph = commonDao.getAll(TermParagraph.class); + + log.info("TermParagraph loaded"); + + log.info("ItemsRanges loading..."); + + itemsRanges = itemsRangeDao.getWithCategories(); + + log.info("ItemsRanges loaded"); + + } + + public void reload() { + init(); + } + + @Override + public List getWithCategories(){ + return itemsRanges; + } + + @Override + public StreamEx getParagraphsByMainTerm(String term){ + return StreamEx.of(allTermParagraph) + .filter(t -> t.getTerm().equals(term)) + .map(TermParagraph::getParagraph) + .sorted(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/links/LinkProvider.java b/src/main/java/org/ayfaar/app/services/links/LinkProvider.java new file mode 100644 index 00000000..3fc694c1 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/links/LinkProvider.java @@ -0,0 +1,77 @@ +package org.ayfaar.app.services.links; + +import org.ayfaar.app.model.LightLink; +import org.ayfaar.app.model.Link; +import org.ayfaar.app.model.LinkType; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.utils.UriGenerator; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +public class LinkProvider { + private final Class uid1Class; + private final Class uid2Class; + private LightLink link; + private Function saver; + + LinkProvider(LightLink link, Function saver) { + this.link = link; + this.saver = saver; + uid1Class = UriGenerator.getClassByUri(link.getUid1()); + uid2Class = UriGenerator.getClassByUri(link.getUid2()); + } + + public String taggedQuote() { + return link.getTaggedQuote(); + } + + public Optional get(Class entityClass) { + if (entityClass == uid1Class) return Optional.of(link.getUid1()); + if (entityClass == uid2Class) return Optional.of(link.getUid2()); + return Optional.empty(); + } + + public LinkType type() { + return link.getType(); + } + + public String not(String uri) { + return Objects.equals(link.getUid1(), uri) ? link.getUid2() : link.getUid1(); + } + + public Updater updater() { + return new Updater(); + } + + public boolean has(Class entityClass) { + return uid1Class == entityClass || uid2Class == entityClass; + } + + public class Updater { + List updates = new LinkedList<>(); + + public Updater rate(Float rate) { + updates.add(() -> link.setRate(rate)); + return this; + } + + public Updater comment(String value) { + updates.add(() -> link.setComment(value)); + return this; + } + + public Updater quote(String value) { + updates.add(() -> link.setQuote(value)); + return this; + } + + public void commit() { + updates.forEach(Runnable::run); + saver.apply(link); + } + } +} diff --git a/src/main/java/org/ayfaar/app/services/links/LinkService.java b/src/main/java/org/ayfaar/app/services/links/LinkService.java new file mode 100644 index 00000000..d2a44521 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/links/LinkService.java @@ -0,0 +1,116 @@ +package org.ayfaar.app.services.links; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.dao.LinkDao; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.NewLinkEvent; +import org.ayfaar.app.model.HasUri; +import org.ayfaar.app.model.LightLink; +import org.ayfaar.app.model.Link; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.services.EntityLoader; +import org.ayfaar.app.utils.SoftCache; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +public class LinkService { + private LinkDao linkDao; + private CommonDao commonDao; + private EntityLoader entityLoader; + private EventPublisher publisher; + private List allLinks; + private SoftCache cache = new SoftCache<>(); + + + @Inject + public LinkService(LinkDao linkDao, CommonDao commonDao, EntityLoader entityLoader, EventPublisher publisher) { + this.linkDao = linkDao; + this.commonDao = commonDao; + this.entityLoader = entityLoader; + this.publisher = publisher; + } + + /* public LinkProvider getByUris(String uri1, String uri2) { + return findByUris(uri1, uri2).orElseThrow(() -> new LogicalException(ExceptionCode.LINK_NOT_FOUND, uri1, uri2)); + }*/ + + @PostConstruct + protected void init() { + allLinks = commonDao.getAll(LightLink.class); + } + /* + public Optional findByUris(String uri1, String uri2) { + final List links = linkDao.getByUris(uri1, uri2); + if (links.size() > 1) throw new RuntimeException("Found more then one link for uri1: `"+uri1+"` and uri2: `"+uri2+"`"); + if (links.isEmpty()) return Optional.empty(); + final LinkProvider provider = new LinkProvider(links.get(0), this::linkSaver); + return Optional.of(provider); + }*/ + + public StreamEx getAllLinksBetween(HasUri uriOwner, Class entityClass) { + return getAllLinksBetween(uriOwner.getUri(), entityClass); + } + + public StreamEx getAllLinksBetween(String uri, Class entityClass) { + return getAllLinksFor(uri) + .filter(linkProvider -> linkProvider.has(entityClass)); + } + + private Link linkSaver(LightLink link) { + final Link entity = linkDao.get(link.getLinkId()); + entity.setComment(link.getComment()); + entity.setQuote(link.getQuote()); + entity.setRate(link.getRate()); + entity.setSource(link.getSource()); + entity.setTaggedQuote(link.getTaggedQuote()); + return linkDao.save(entity); + } + + public StreamEx getAllLinksFor(String uri) { + return StreamEx.of(allLinks) + .filter(link -> Objects.equals(link.getUid1(), uri) || Objects.equals(link.getUid2(), uri)) + .map(this::getLinkProvider); + } + + public Optional getByUris(String uri1, String uri2) { + return StreamEx.of(allLinks) + .filter(link -> (Objects.equals(link.getUid1(), uri1) && Objects.equals(link.getUid2(), uri2)) + || (Objects.equals(link.getUid1(), uri2) && Objects.equals(link.getUid2(), uri1))) + .map(this::getLinkProvider) + .findFirst(); + } + + private LinkProvider getLinkProvider(LightLink link) { + return cache.getOrCreate(link, () -> new LinkProvider(link, this::linkSaver)); + } + + public void registerNew(Link link) { + allLinks.add(LightLink.fromLink(link)); + publisher.publishEvent(new NewLinkEvent(link)); + } + + public void reload() { + init(); + } + + public Optional getLinkBetween(HasUri hasUri, Class entityClass) { + return getAllLinksFor(hasUri.getUri()) + .filter(linkProvider -> linkProvider.has(entityClass)) + .findFirst(); + } + + public Optional getLinked(HasUri hasUri, Class entityClass) { + return getAllLinksFor(hasUri.getUri()) + .filter(linkProvider -> linkProvider.has(entityClass)) + .findFirst() + .get() + .get(entityClass); + } +} diff --git a/src/main/java/org/ayfaar/app/services/moderation/Action.java b/src/main/java/org/ayfaar/app/services/moderation/Action.java new file mode 100644 index 00000000..e2336dd2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/moderation/Action.java @@ -0,0 +1,135 @@ +package org.ayfaar.app.services.moderation; + +import java.util.ArrayList; +import java.util.List; + +import static org.ayfaar.app.services.moderation.UserRole.ROLE_ADMIN; +import static org.ayfaar.app.services.moderation.UserRole.ROLE_AUTHENTICATED; +import static org.ayfaar.app.services.moderation.UserRole.ROLE_EDITOR; + +public enum Action { + TOPIC (ROLE_EDITOR), + TOPIC_CREATE ("Создание ключевого слова {}", TOPIC, ROLE_ADMIN), + TOPIC_ADD_CHILD ("Создание дочернего ключевого слова {} для {}", TOPIC, ROLE_ADMIN), + TOPIC_LINK_RESOURCE ("Добавление {} к ключевому слову {}", TOPIC), + TOPIC_LINK_RANGE ("Добавление диапалоза с {} по {} к ключевому слову {}", TOPIC), + TOPIC_RESOURCE_LINK_COMMENT_UPDATE("Обновление комментария в связке между {} и ключевым словом {}, новый коментарий: `{}` предложен", TOPIC), + TOPIC_RESOURCE_LINK_RATE_UPDATE("Обновление весового коэфициента в связке между {} и ключевым словом {}, новые значение: {} предложено", TOPIC), + TOPIC_RESOURCE_LINK_UPDATE(TOPIC), // for fix db problem + TOPIC_RESOURCE_LINKED("Ключевое слово {} прикреплена к {}"), + + ITEMS_RANGE (ROLE_EDITOR), + ITEMS_RANGE_CREATE (ITEMS_RANGE), + ITEMS_RANGE_UPDATE (ITEMS_RANGE), + + USER_RENAME("Имя пользователя {} измененно на {}"), + TOPIC_RESOURCE_UNLINKED("От ключевого слова {} откреплён {}"), + TOPIC_CHILD_ADDED("Ключевому слову {} добавлено дочернее ключевое слово {}"), + TOPIC_TOPIC_UNLINKED("От ключевого слова {} откреплено ключевоое слово {}"), + TOPIC_UNLINK_TOPIC("Открепление от ключевого слова {} дочернего {}", ROLE_ADMIN), + TOPIC_UNLINK_RESOURCE("Отмена связи между {} и ключевым словом {}", TOPIC), + TOPIC_MERGE("Объединение ключевого слова {} в ключевое слово {}", ROLE_ADMIN), + TOPIC_MERGED("Ключевое слово `{}` объединена с {}"), + + VIDEO_ADDED("Добавлено видео {}"), + VIDEO_ADD("Добавление нового видео по ссылке {}", UserRole.ROLE_AUTHENTICATED), + VIDEO_REMOVE("Удаление видео видео:youtube:{}", UserRole.ROLE_EDITOR), + VIDEO_UPDATE_TITLE("Обновление названия видео {} на `{}`", UserRole.ROLE_EDITOR), + VIDEO_UPDATE_CODE("Задание или изменеие кода видео видео:youtube:{}, код `{}`", UserRole.ROLE_EDITOR), + VIDEO_CODE_UPDATED("Кода видео {} обновился c `{}` на `{}`", UserRole.ROLE_EDITOR), + NEW_USER("Выполнен вход в систему новым "), + VIDEO_REMOVED("Видео `{}` c id: {} далено из системы"), + RECORD_RENAME("Переименование ответа запись:{} на `{}`", ROLE_EDITOR), + RECORD_RENAMED("Ответ {} переименован c `{}` в `{}`"), + + DOCUMENT_CREATED("Добавлен документ {}"), + DOCUMENT_RENAME("Переименование документа {} на `{}`", ROLE_EDITOR), + DOCUMENT_RENAMED("Документ переименован c `{}` на {}"), + DOCUMENT_ADD("Добавление документа {}", ROLE_AUTHENTICATED), + + SYSLOG_TRANSLATION_NEW("\"{}\" -> \"{}\", "), + SYSLOG_TRANSLATION_UPDATE("\"{}\" из \"{}\" в \"{}\", "), SYS_EVENT(""), + QUOTE_CREATED("К термину ии:термин:{} добавленна цитата с {}", ROLE_EDITOR), + CREATE_QUOTE("К термину ии:термин:{} предлагается цитата `{}` связанная с {}", ROLE_EDITOR); + + private Action parent = null; + private UserRole requiredAccessLevel; + private List children = new ArrayList<>(); + public String message; + + + Action(Action parent) { + this(null, parent, null); + } + Action(UserRole requiredAccessLevel) { + this(null, null, requiredAccessLevel); + } + Action(String message, UserRole requiredAccessLevel) { + this(message, null, requiredAccessLevel); + } + Action(String message, Action parent, UserRole requiredAccessLevel) { + this.message = message; + this.parent = parent; + this.requiredAccessLevel = requiredAccessLevel; + if (this.parent != null) { + this.parent.addChild(this); + } + } + + + Action(String message) { + this.message = message; + } + + + Action(String message, Action parent) { + this(message, parent, null); + } + + public Action parent() { + return parent; + } + + public boolean is(Action other) { + if (other == null) { + return false; + } + + for (Action action = this; action != null; action = action.parent()) { + if (other == action) { + return true; + } + } + return false; + } + + public Action[] children() { + return children.toArray(new Action[children.size()]); + } + + public Action[] allChildren() { + List list = new ArrayList<>(); + addChildren(this, list); + return list.toArray(new Action[list.size()]); + } + + private static void addChildren(Action root, List list) { + list.addAll(root.children); + for (Action child : root.children) { + addChildren(child, list); + } + } + + private void addChild(Action child) { + this.children.add(child); + } + + public UserRole getRequiredAccessLevel() { + return requiredAccessLevel != null ? requiredAccessLevel : parent.getRequiredAccessLevel(); + } + + @Override + public String toString() { + return name()+" with access "+getRequiredAccessLevel().name(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/moderation/MethodEntry.java b/src/main/java/org/ayfaar/app/services/moderation/MethodEntry.java new file mode 100644 index 00000000..412095d5 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/moderation/MethodEntry.java @@ -0,0 +1,11 @@ +package org.ayfaar.app.services.moderation; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MethodEntry { + @NonNull public Action action; + @NonNull public String command; + @NonNull public Object[] args; +} diff --git a/src/main/java/org/ayfaar/app/services/moderation/ModerationAspect.java b/src/main/java/org/ayfaar/app/services/moderation/ModerationAspect.java new file mode 100644 index 00000000..94462629 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/moderation/ModerationAspect.java @@ -0,0 +1,30 @@ +package org.ayfaar.app.services.moderation; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.ayfaar.app.annotations.Moderated; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; + +@Aspect +@Component +public class ModerationAspect { + private final ModerationService moderationService; + + @Inject + public ModerationAspect(ModerationService moderationService) { + this.moderationService = moderationService; + } + + @Pointcut("@annotation(org.ayfaar.app.annotations.Moderated)") + public void moderatedAnnotated() {} + + @Around(value = "moderatedAnnotated() && @annotation(moderated)") + public Object around(ProceedingJoinPoint pjp, Moderated moderated) throws Throwable { + moderationService.checkMethod(moderated, pjp.getArgs()); + return pjp.proceed(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/moderation/ModerationService.java b/src/main/java/org/ayfaar/app/services/moderation/ModerationService.java new file mode 100644 index 00000000..0f32f8ca --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/moderation/ModerationService.java @@ -0,0 +1,139 @@ +package org.ayfaar.app.services.moderation; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.annotations.Moderated; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SimplePushEvent; +import org.ayfaar.app.model.ActionEvent; +import org.ayfaar.app.model.PendingAction; +import org.ayfaar.app.utils.CurrentUserProvider; +import org.ayfaar.app.utils.exceptions.ConfirmationRequiredException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; +import java.util.Date; + +import static java.lang.String.format; +import static org.slf4j.helpers.MessageFormatter.arrayFormat; + + +@Service +@Slf4j +public class ModerationService { + private final StandardEvaluationContext context; + private final SpelExpressionParser parser; + private final CommonDao commonDao; + private final CurrentUserProvider currentUserProvider; + private final EventPublisher publisher; + + private final ThreadLocal threadLocal = new ThreadLocal<>(); + + @Inject + protected ModerationService(CommonDao commonDao, BeanFactory beanFactory, CurrentUserProvider currentUserProvider, EventPublisher publisher) { + this.commonDao = commonDao; + this.currentUserProvider = currentUserProvider; + this.publisher = publisher; + context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(beanFactory)); + parser = new SpelExpressionParser(); + } + + public void notice(Action action, Object... args) { + String message = arrayFormat(action.message, args).getMessage() + " пользователем " + getCurrentUserName(); + log.info(message); + final ActionEvent actionEvent = new ActionEvent(); + actionEvent.setMessage(message); + // указать такущего пользователя + actionEvent.setCreatedBy(getCurrentUserId()); + // указать action + actionEvent.setAction(action); + commonDao.save(actionEvent); + } + + public void check(Action action, Object... args) { + if (!currentUserProvider.getCurrentAccessLevel().accept(action.getRequiredAccessLevel())) { + final PendingAction pendingAction = registerConfirmationRequirement(action, args); + throw new ConfirmationRequiredException(pendingAction); + } + } + + private PendingAction registerConfirmationRequirement(Action action, Object[] args) { + final MethodEntry entry = threadLocal.get(); + if (entry == null) throw new RuntimeException("No moderated method for action " + action); + Action rootAction = entry.action; + String actionText = ""; + if (!rootAction.name().equals(action.name())) { + actionText += action.message != null ? arrayFormat(action.message, args).getMessage() : action.name(); + actionText += " для "; + } + final PendingAction pendingAction = new PendingAction(); + actionText += rootAction.message != null ? arrayFormat(rootAction.message, entry.args).getMessage() : rootAction.name(); + // fixme: в случае отсутствия пользователя в getCurrentUserName() ошибка + pendingAction.setMessage(format("%s пользователем %s", actionText, getCurrentUserName())); + pendingAction.setInitiatedBy(getCurrentUserId()); + pendingAction.setCommand(buildCommand(entry)); + pendingAction.setAction(rootAction); + publisher.publishEvent(new SimplePushEvent("Confirmation request", pendingAction.getMessage())); + return commonDao.save(pendingAction); + } + + private String buildCommand(MethodEntry entry) { + String command = entry.command + "("; + for (int i = 0; entry.args.length > i ; i++) { + Object arg = entry.args[i]; + if (arg == null) { + command += "null,"; + } else if (arg instanceof String) { + command += format("'%s',", ((String) arg).replace("'", "\\'")); + } else if (arg instanceof Number) { + command += format("%s,", arg); + } else { + throw new RuntimeException("Cannot serialize "+arg); + } + } + return command.substring(0, command.lastIndexOf(",")) + ")"; + } + + public void cancel(Integer id) { + final PendingAction pendingAction = commonDao.getOpt(PendingAction.class, id) + .orElseThrow(() -> new RuntimeException("Action not found")); + boolean ownAction = getCurrentUserId().equals(pendingAction.getInitiatedBy()); + if (!ownAction && !currentUserProvider.getCurrentAccessLevel().accept(pendingAction.getAction().getRequiredAccessLevel())) + throw new ConfirmationRequiredException(pendingAction); + commonDao.remove(pendingAction); + } + + public void confirm(PendingAction pendingAction) { + if (!currentUserProvider.getCurrentAccessLevel().accept(pendingAction.getAction().getRequiredAccessLevel())) + throw new ConfirmationRequiredException(pendingAction); + // perform command + parser.parseExpression(pendingAction.getCommand()).getValue(context); + log.info("{} confirmed by user {}", pendingAction.getMessage(), getCurrentUserId()); + pendingAction.setConfirmedBy(getCurrentUserId()); + pendingAction.setConfirmedAt(new Date()); + commonDao.save(pendingAction); + } + + private Integer getCurrentUserId() { + return currentUserProvider.get().isPresent() ? currentUserProvider.get().get().getId() : null; + } + + private String getCurrentUserName() { + // fixme: не продуман кейс отсутсвия пользователя + return currentUserProvider.get().isPresent() ? currentUserProvider.get().get().getName() : "Аноним"; + } + + void checkMethod(Moderated moderated, Object[] args) { +// MethodEntry entry = threadLocal.get(); +// if (entry == null) { + MethodEntry entry = new MethodEntry(moderated.value(), moderated.command(), args); + threadLocal.set(entry); +// } + check(moderated.value(), args); + } +} diff --git a/src/main/java/org/ayfaar/app/services/moderation/UserRole.java b/src/main/java/org/ayfaar/app/services/moderation/UserRole.java new file mode 100644 index 00000000..4d5b5845 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/moderation/UserRole.java @@ -0,0 +1,31 @@ +package org.ayfaar.app.services.moderation; + +import one.util.streamex.StreamEx; + +import java.util.Optional; + +import static one.util.streamex.MoreCollectors.onlyOne; + +public enum UserRole { + ROLE_ADMIN(0), ROLE_EDITOR(1), ROLE_AUTHENTICATED(2), ROLE_ANONYMOUS(999); + + private int precedence; + + UserRole(int precedence) { + this.precedence = precedence; + } + + public static Optional fromPrecedence(int precedence) { + return StreamEx.of(UserRole.values()) + .filter(accessLevel -> accessLevel.getPrecedence() == precedence) + .collect(onlyOne()); + } + + public int getPrecedence() { + return precedence; + } + + public boolean accept(UserRole requiredAccessLevel) { + return precedence <= requiredAccessLevel.precedence; + } +} diff --git a/src/main/java/org/ayfaar/app/services/record/RecordService.java b/src/main/java/org/ayfaar/app/services/record/RecordService.java new file mode 100644 index 00000000..67f917a6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/record/RecordService.java @@ -0,0 +1,25 @@ +package org.ayfaar.app.services.record; + +import org.ayfaar.app.model.Record; + +import java.util.List; + +import java.util.Map; +import java.util.Optional; + +public interface RecordService { + + void reload(); + + Map getAllUriNames(); + + List getAll(); + + boolean isPrivateRecordsVisible(); + + Map getAllUriCodes(); + + Optional getByCode(String code); + + void save(Record record); +} diff --git a/src/main/java/org/ayfaar/app/services/record/RecordServiceImpl.java b/src/main/java/org/ayfaar/app/services/record/RecordServiceImpl.java new file mode 100644 index 00000000..4652560e --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/record/RecordServiceImpl.java @@ -0,0 +1,86 @@ +package org.ayfaar.app.services.record; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Record; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.UserRole; +import org.ayfaar.app.utils.CurrentUserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.ayfaar.app.utils.StreamUtils.single; + +@Slf4j +@Component() +public class RecordServiceImpl implements RecordService { + private final CommonDao commonDao; + private CurrentUserProvider currentUserProvider; + + private List allRecords; + + @Autowired + public RecordServiceImpl(CommonDao commonDao, CurrentUserProvider currentUserProvider) { + this.commonDao = commonDao; + this.currentUserProvider = currentUserProvider; + } + + @PostConstruct + private void init() { + log.info("Records loading..."); + + allRecords = commonDao.getAll(Record.class); + + log.info("Records loaded"); + } + + @Override + public void reload() { + init(); + } + + @Override + public Map getAllUriNames() { + final boolean internalRecordAllowed = isPrivateRecordsVisible(); + return allRecords.stream() + .filter(r -> internalRecordAllowed || !StringUtils.isEmpty(r.getAudioUrl())) + .collect(Collectors.toMap(UID::getUri, Record::getName)); + } + + @Override + public Map getAllUriCodes() { + final boolean privateRecordsVisible = isPrivateRecordsVisible(); + return allRecords.stream() + .filter(r -> privateRecordsVisible || !StringUtils.isEmpty(r.getAudioUrl())) + .collect(Collectors.toMap(UID::getUri, Record::getCode)); + } + + @Override + public Optional getByCode(String code) { + return allRecords.stream().filter(record -> record.getCode().equals(code)).collect(single()); + } + + @Override + public void save(Record record) { + commonDao.save(record); + } + + @Override + public List getAll() { + return allRecords; + } + + @Override + public boolean isPrivateRecordsVisible() { + final Optional currentUserOpt = currentUserProvider.get(); + return currentUserOpt.isPresent() && currentUserOpt.get().getRole().accept(UserRole.ROLE_EDITOR); + } +} diff --git a/src/main/java/org/ayfaar/app/services/sysLogs/SysLogService._java b/src/main/java/org/ayfaar/app/services/sysLogs/SysLogService._java new file mode 100644 index 00000000..5fa9931c --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/sysLogs/SysLogService._java @@ -0,0 +1,13 @@ +/* +package org.ayfaar.app.services.sysLogs; + +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.SysLog; + +import java.util.List; + +public interface SysLogService { + List getAll(); + SysLog save(SysLogEvent event); +} +*/ diff --git a/src/main/java/org/ayfaar/app/services/sysLogs/SysLogServiceImpl._java b/src/main/java/org/ayfaar/app/services/sysLogs/SysLogServiceImpl._java new file mode 100644 index 00000000..cbb468cf --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/sysLogs/SysLogServiceImpl._java @@ -0,0 +1,54 @@ +/* +package org.ayfaar.app.services.sysLogs; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.SysLog; +import org.ayfaar.app.model.User; +import org.ayfaar.app.utils.CurrentUserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Optional; + +@Service +public class SysLogServiceImpl implements SysLogService { + CommonDao commonDao; + CurrentUserProvider currentUserProvider; + + @Autowired + public SysLogServiceImpl(CommonDao commonDao, CurrentUserProvider currentUserProvider) { + this.commonDao = commonDao; + this.currentUserProvider = currentUserProvider; + } + + @Override + public List getAll() { + return commonDao.getAll(SysLog.class); + } + + @Override + public SysLog save(SysLogEvent event) { + return commonDao.save(toSysLog(event)); + } + + private SysLog toSysLog(SysLogEvent event) { + Optional userOptional = currentUserProvider.get(); + User user = null; + if (userOptional.isPresent()) { + user = userOptional.get(); + } + + SysLog sysLog = new SysLog(); + sysLog.setUser(user); + sysLog.setDate(new Date()); + sysLog.setLogger(event.getSource().getClass().getName()); + sysLog.setMessage(event.getMessage()); + sysLog.setLevel(event.getLevel()); + + return sysLog; + } +} +*/ diff --git a/src/main/java/org/ayfaar/app/services/topics/TopicProvider.java b/src/main/java/org/ayfaar/app/services/topics/TopicProvider.java new file mode 100644 index 00000000..7a26b35c --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/topics/TopicProvider.java @@ -0,0 +1,109 @@ +package org.ayfaar.app.services.topics; + +import lombok.Builder; +import org.ayfaar.app.model.*; +import org.ayfaar.app.utils.TermService; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public interface TopicProvider { + @NotNull + String name(); + + default void link(LinkType type, UID uid) { + link(type, uid, null); + } + + default Link link(LinkType type, UID uid, String comment) { + return link(type, uid, comment, null, null); + } + + Optional relatedTermSuggestion(); + + Stream children(); + Stream parents(); + Stream related(); + + default void addChild(Topic child) { + link(LinkType.CHILD, child); + } + + default void addChild(TopicProvider topicProvider) { + addChild(topicProvider.topic()); + } + + @NotNull + Topic topic(); + + @NotNull + String uri(); + + TopicProvider addChild(String name); + TopicProvider unlink(String linkedTopicName); + default void unlink(TopicProvider linked) { + unlink(linked.name()); + } + + TopicProvider merge(String mergeWith); + void delete(); + /** + * @return все ресурсы связаные любыми линками с этой темой + */ + // todo: return only 6 resources for each type and create new method fo loading rest with paging. + // And return flag hasMore in each ResourcePresentation + TopicResources resources(); + + default void link(UID uid) { + link(null, uid); + } + + Link link(LinkType linkType, UID uid, String comment, String quote, Float rate); + + Optional getChild(String child); + + class TopicResources { + public List image = new LinkedList<>(); + public List video = new LinkedList<>(); + public List item = new LinkedList<>(); + public List itemsRange = new LinkedList<>(); + public List document = new LinkedList<>(); + public List record = new LinkedList<>(); + } + + class ResourcePresentation implements Comparable> { + public String quote; + public String comment; + public Float rate; + public T resource; + public List topics; + + ResourcePresentation(T uid, Link link) { + quote = link.getQuote(); + comment = link.getComment(); + rate = link.getRate(); + resource = uid; + } + + @Override + public int compareTo(ResourcePresentation o) { + return resource instanceof Comparable ? ((Comparable) resource).compareTo(o.resource) : 0; + } + + @Builder + static class Resource { + public String uri; + public String title; + } + } + + Optional linkedTerm(); + + // todo implement + default boolean hasGrandParent(String name) { + throw new RuntimeException("Unimplemented"); + } +} diff --git a/src/main/java/org/ayfaar/app/services/topics/TopicService.java b/src/main/java/org/ayfaar/app/services/topics/TopicService.java new file mode 100644 index 00000000..1ffbe0b6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/topics/TopicService.java @@ -0,0 +1,64 @@ +package org.ayfaar.app.services.topics; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.model.Topic; +import org.ayfaar.app.utils.UriGenerator; +import org.ayfaar.app.utils.exceptions.ExceptionCode; +import org.ayfaar.app.utils.exceptions.LogicalException; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface TopicService { + @NotNull + Optional get(String uri); + + @NotNull + Optional contains(String s); + + @NotNull + Optional get(String uri, boolean caseSensitive); + + @NotNull + default TopicProvider findOrCreate(String name) { + return findOrCreate(name, false, true); + } + + @NotNull + default TopicProvider findOrCreate(String name, boolean caseSensitive) { + return findOrCreate(name, caseSensitive, true); + } + + @NotNull + TopicProvider findOrCreate(String name, boolean caseSensitive, boolean checkAuth); + + /** + * Throw exception on topic not found + * @param name + * @return + */ + @NotNull + default TopicProvider getByName(String name){ + return findByName(name).orElseThrow(() -> new LogicalException(ExceptionCode.TOPIC_NOT_FOUND, name)); + } + + default Optional findByName(String name){ + return get(UriGenerator.generate(Topic.class, name)); + } + + @NotNull + TopicProvider getByName(String name, boolean caseSensitive); + + void reload(); + + + boolean exist(String name); + + StreamEx getAllLinkedWith(String uri); + + List getAllNames(); + + Map getAllUriNames(); +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/services/topics/TopicServiceImpl.java b/src/main/java/org/ayfaar/app/services/topics/TopicServiceImpl.java new file mode 100644 index 00000000..0247e42c --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/topics/TopicServiceImpl.java @@ -0,0 +1,381 @@ +package org.ayfaar.app.services.topics; + +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.dao.LinkDao; +import org.ayfaar.app.model.*; +import org.ayfaar.app.services.links.LinkService; +import org.ayfaar.app.services.moderation.Action; +import org.ayfaar.app.services.moderation.ModerationService; +import org.ayfaar.app.services.moderation.UserRole; +import org.ayfaar.app.utils.CurrentUserProvider; +import org.ayfaar.app.utils.TermService; +import org.ayfaar.app.utils.UriGenerator; +import org.ayfaar.app.utils.exceptions.ExceptionCode; +import org.ayfaar.app.utils.exceptions.LogicalException; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static one.util.streamex.MoreCollectors.onlyOne; +import static org.ayfaar.app.utils.StreamUtils.single; + +@Component("topicService") +class TopicServiceImpl implements TopicService { + private static final Logger logger = LoggerFactory.getLogger(TopicServiceImpl.class); + + /** + * Key is topic uri in lower case + */ + private TopicsMap topics = new TopicsMap(); + + @Inject CommonDao commonDao; + @Inject LinkDao linkDao; + @Inject ModerationService moderationService; + @Inject Environment environment; + @Inject CurrentUserProvider currentUserProvider; + @Inject LinkService linkService; + @Inject TermService termService; + + @PostConstruct + private void init() { + logger.info("Topics loading..."); + + commonDao.getAll(Topic.class) + .stream() +// .parallel() + .map(TopicProviderImpl::new) + .forEach(t -> topics.put(t.uri(), t)); + + linkDao.getAll() + .stream() + .parallel() + .filter(l -> l.getUid1() instanceof Topic || l.getUid2() instanceof Topic) + .forEach(link -> { + if (link.getUid1() instanceof Topic) { + final TopicProviderImpl provider = topics.get(link.getUid1().getUri()); + provider.registerLink(link, link.getUid2()); + } + if (link.getUid2() instanceof Topic) { + final TopicProviderImpl provider = topics.get(link.getUid2().getUri()); + provider.registerLink(link, link.getUid1()); + } + }); + logger.info("Topics loaded"); + } + + @NotNull + @Override + public Optional get(String uri) { + return Optional.ofNullable(topics.get(uri)); + } + + @NotNull + @Override + public Optional contains(String s) { + return topics.keySet().stream() + .filter(name -> name.toLowerCase().contains(s.toLowerCase())) + .findFirst() + .map(topic -> topics.get(topic)); + } + + @NotNull + @Override + public Optional get(String uri, boolean caseSensitive) { + if (!caseSensitive) return get(uri); + return topics.values().parallelStream() + .filter(p -> p.uri().equals(uri)) + .collect(single()); + } + + @NotNull + @Override + public TopicProvider findOrCreate(String name, boolean caseSensitive, boolean checkAuth) { + return get(UriGenerator.generate(Topic.class, name), caseSensitive) + .orElseGet(() -> { + if (checkAuth) moderationService.check(Action.TOPIC_CREATE, name); + final Topic topic = commonDao.save(new Topic(name)); + final TopicProviderImpl provider = new TopicProviderImpl(topic); + topics.put(provider.uri(), provider); + return provider; + }); + } + + @NotNull + @Override + public TopicProvider getByName(String name, boolean caseSensitive) { + if (!caseSensitive) return getByName(name); + return get(UriGenerator.generate(Topic.class, name), true) + .orElseThrow(() -> new LogicalException(ExceptionCode.TOPIC_NOT_FOUND, name)); + } + + @Override + public void reload() { + topics.clear(); + init(); + } + + @Override + public boolean exist(String name) { + return topics.values().stream().anyMatch(c -> c.name().equals(name)); + } + + @Override + // fixme: sorting by Link.rate DESC + public StreamEx getAllLinkedWith(String uri){ + return StreamEx.of(topics.values()) + .flatMap(topicProvider -> StreamEx.of(topicProvider.linksMap.values()) + .filter(link -> link.getUid2().getUri().equals(uri)) + .sortedByDouble(Link::getRate) + .reverseSorted() + .map(link -> get(link.getUid1().getUri()).get())); + } + @Override + public List getAllNames(){ + return topics.values().stream().map(topicProvider -> + topicProvider.topic().getName().trim()).collect(Collectors.toList()); + } + @Override + public Map getAllUriNames(){ + return topics.values().stream().collect(Collectors.toMap(topicProvider -> + topicProvider.topic().getUri(),topicProvider -> topicProvider.topic().getName())); + } + + private class TopicProviderImpl implements TopicProvider { + private final Topic topic; + private Map linksMap = new HashMap<>(); + + private TopicProviderImpl(Topic topic) { + Assert.notNull(topic); + this.topic = topic; + } + + @NotNull + @Override + public String name() { + return topic.getName(); + } + + @Override + public Link link(LinkType type, UID uid, String comment, String quote, Float rate) { + Link link = linksMap.get(uid); + if (link != null && link.getType()!= null && !link.getType().isChild()) + throw new RuntimeException("Link already exist with another type: " + link.getType()); + + // link = linkRepository.save(new Link(topic, uid, type, comment)); this throw error + + link = Link.builder() + .uid1(topic) + .uid2(uid) + .type(type) + .comment(comment) + .quote(quote) + .rate(rate) + .build(); + Link finalLink = link; + +// if (!environment.acceptsProfiles("dev")) + currentUserProvider.get().ifPresent(u -> finalLink.setCreatedBy(u.getId())); + + if (uid instanceof Topic) { + topics.get(uid.getUri()).registerLink(link, topic); + } + linkDao.save(link); + linkService.registerNew(link); + registerLink(link, uid); + return link; + } + + @Override + public Optional getChild(String child) { + return children().filter(p -> p.name().equals(child)).collect(onlyOne()); + } + + @Override + public Optional linkedTerm() { + return linkService.getLinkBetween(topic, Term.class) + .map(linkProvider -> linkProvider.get(Term.class).get()) + .map(termService::getByUri) + .orElseGet(() -> termService.getMainOrThis(name())); + } + + @Override + public Optional relatedTermSuggestion() { + return termService.getAll().stream() + .filter(entry -> entry.getKey().length() > 2) + .filter(entry -> name().toLowerCase().contains(entry.getKey().toLowerCase())) + .findFirst() + .map(Map.Entry::getValue); + } + + @Override + public Stream children() { + return StreamEx.of(linksMap.values()) + .filter(link -> + link.getUid1() instanceof Topic + && link.getUid2() instanceof Topic + && link.getUid1().getUri().equals(uri()) + && link.getType() == LinkType.CHILD) + .map(l -> new TopicProviderImpl((Topic) l.getUid2())) + .sorted((o1, o2) -> o1.name().toLowerCase().compareTo(o2.name().toLowerCase())); + } + + @Override + public Stream parents() { + return linksMap.values().stream() + .filter(link -> + link.getUid1() instanceof Topic + && link.getUid2() instanceof Topic + && link.getUid2().getUri().equals(uri()) + && link.getType().isChild()) + .map(l -> new TopicProviderImpl((Topic) l.getUid1())); + } + + @Override + public Stream related() { + return linksMap.entrySet().stream() + .filter(entry -> entry.getKey() instanceof Topic && entry.getValue().getType() == null) + .map(e -> new TopicProviderImpl((Topic) e.getKey())); + } + + @NotNull + @Override + public Topic topic() { + return topic; + } + + @NotNull + @Override + public String uri() { + return topic.getUri(); + } + + @Override + public TopicProvider addChild(String name) { + final TopicProvider child = findOrCreate(name); + addChild(child); + return child; + } + + @Override + public TopicProvider unlink(String linkedTopicName) { + final TopicProvider linkedTopic = getByName(linkedTopicName); + final Link link = linksMap.remove(linkedTopic.topic()); + linkDao.remove(link.getLinkId()); + return linkedTopic; + } + + @Override + public void delete() { + commonDao.remove(topic); + topics.remove(topic().getUri()); + } + + @Override + public TopicProviderImpl merge(String mergeInto) { + moderationService.check(Action.TOPIC_MERGE, topic.getName(), mergeInto); + commonDao.remove(topic); // remove from db for case sensitive case + final TopicProvider provider = findOrCreate(mergeInto, true); + linksMap.values().forEach(link -> { + // заменяем ссылки на старый топик на ссылки на новый + UID uid1 = link.getUid1().getUri().equals(uri()) ? provider.topic() : link.getUid1(); + UID uid2 = link.getUid2().getUri().equals(uri()) ? provider.topic() : link.getUid2(); + link = linkDao.save(Link.builder() + .uid1(uid1) + .uid2(uid2) + .type(link.getType()) + .comment(link.getComment()) + .quote(link.getQuote()) + .rate(link.getRate()) + .build()); + }); + moderationService.notice(Action.TOPIC_MERGED, topic.getName(), provider.name()); + reload(); + return this; + } + + @Override + public TopicResources resources() { + final TopicResources resources = new TopicResources(); + resources.image.addAll(prepareResource(Image.class)); + resources.video.addAll(prepareResource(VideoResource.class)); + resources.item.addAll(prepareItemResource()); + resources.itemsRange.addAll(prepareResource(ItemsRange.class)); + resources.document.addAll(prepareResource(Document.class)); + resources.record.addAll(StreamEx.of(prepareResource(Record.class)) + .filter(r -> r.resource.getAudioUrl() != null || + (currentUserProvider.get().isPresent() && currentUserProvider.get().get().getRole().accept(UserRole.ROLE_EDITOR))) + .sorted((r1, r2) -> { + if (r1.resource.getAudioUrl() != null && r2.resource.getAudioUrl() == null) return -1; + if (r2.resource.getAudioUrl() != null && r1.resource.getAudioUrl() == null) return 1; + return r2.resource.getRecorderAt().compareTo(r1.resource.getRecorderAt()); + }).toList()); + return resources; + } + + private Collection> prepareItemResource() { + List> list = new LinkedList<>(); + //noinspection unchecked + linksMap.entrySet().stream() + .filter(e -> e.getKey() instanceof Item) + .map(e -> new ResourcePresentation(new ItemResourcePresentation((Item) e.getKey()), e.getValue())) + .forEach(list::add); + return list; + } + + private List> prepareResource(Class resourceClass) { + List> list = new LinkedList<>(); + //noinspection unchecked + linksMap.entrySet().stream() + .filter(e -> e.getKey().getClass().isAssignableFrom(resourceClass)) + .map(e -> new ResourcePresentation(e.getKey(), e.getValue())) + .sorted() + .forEachOrdered(r -> { + r.topics = getAllLinkedWith(r.resource.getUri()).map(TopicProvider::name).toList(); + list.add(r); + }); + return list; + } + + private void registerLink(Link link, UID uid) { + //todo: не хранить тут саму сущность UID, а хранить только пё презентацию которая будет выдана в ресурсках + linksMap.put(uid, link); + } + + private class ItemResourcePresentation implements HasUri { + public final String number; + public final String uri; + + private ItemResourcePresentation(Item item) { + number = item.getNumber(); + uri = item.getUri(); + } + + @Override + public String getUri() { + return uri; + } + } + } + + private class TopicsMap extends LinkedHashMap { + @Override + public TopicProviderImpl put(String key, TopicProviderImpl value) { + return super.put(key.toLowerCase(), value); + } + + @Override + public TopicProviderImpl get(Object key) { + return super.get(((String) key).toLowerCase()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/services/translations/TranslationService.java b/src/main/java/org/ayfaar/app/services/translations/TranslationService.java new file mode 100644 index 00000000..29784090 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/translations/TranslationService.java @@ -0,0 +1,43 @@ +package org.ayfaar.app.services.translations; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.Translation; +import org.ayfaar.app.translation.TranslationItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.stream.Stream; + +@Slf4j +@Service +public class TranslationService { + private CommonDao commonDao; + private List translations; + + @Autowired + public TranslationService(CommonDao commonDao) { + this.commonDao = commonDao; + } + + @PostConstruct + public void init() { + log.info("Translations loading..."); + translations = commonDao.getAll(Translation.class); + log.info("Translations loaded"); + } + + public List getAll() { + return translations; + } + + public Stream getAllAsTranslationItem() { + return getAll().stream().map(t -> new TranslationItem(t.getOrigin().trim(), t.getTranslated().trim())); + } + + public Translation save(Translation translation) { + return commonDao.save(translation); + } +} diff --git a/src/main/java/org/ayfaar/app/services/user/UserPresentation.java b/src/main/java/org/ayfaar/app/services/user/UserPresentation.java new file mode 100644 index 00000000..3949be5d --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/user/UserPresentation.java @@ -0,0 +1,18 @@ +package org.ayfaar.app.services.user; + +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.UserRole; + +public class UserPresentation { + public Integer id; + public String email; + public String name; + public UserRole role; + + public UserPresentation(User user) { + id = user.getId(); + email = user.getEmail(); + email = user.getName(); + role = user.getRole(); + } +} diff --git a/src/main/java/org/ayfaar/app/services/user/UserService.java b/src/main/java/org/ayfaar/app/services/user/UserService.java new file mode 100644 index 00000000..39ccaba6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/user/UserService.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.services.user; + +public interface UserService { + UserPresentation getPresentation(Integer id); +} diff --git a/src/main/java/org/ayfaar/app/services/user/UserServiceImpl.java b/src/main/java/org/ayfaar/app/services/user/UserServiceImpl.java new file mode 100644 index 00000000..a64821d2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/user/UserServiceImpl.java @@ -0,0 +1,18 @@ +package org.ayfaar.app.services.user; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.User; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; + +@Service +public class UserServiceImpl implements UserService { + @Inject CommonDao commonDao; + + @Override + public UserPresentation getPresentation(Integer id) { + // todo implement logic + return id == null ? null : new UserPresentation(commonDao.getOpt(User.class, id).get()); + } +} diff --git a/src/main/java/org/ayfaar/app/services/videoResource/VideoResourceService.java b/src/main/java/org/ayfaar/app/services/videoResource/VideoResourceService.java new file mode 100644 index 00000000..a14eb2c4 --- /dev/null +++ b/src/main/java/org/ayfaar/app/services/videoResource/VideoResourceService.java @@ -0,0 +1,32 @@ +package org.ayfaar.app.services.videoResource; + +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.UID; +import org.ayfaar.app.model.VideoResource; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.springframework.util.StringUtils.isEmpty; + +@Component +public class VideoResourceService { + @Inject CommonDao commonDao; + + public Map getAllUriNames() { + return getAll().stream().collect(Collectors.toMap(UID::getUri, VideoResource::getTitle)); + } + + public List getAll() { + return commonDao.getAll(VideoResource.class); + } + + public Map getAllUriCodes() { + return getAll().stream() + .filter(v -> !isEmpty(v.getCode())) + .collect(Collectors.toMap(UID::getUri, VideoResource::getCode)); + } +} diff --git a/src/main/java/org/ayfaar/app/spring/Model.java b/src/main/java/org/ayfaar/app/spring/Model.java deleted file mode 100644 index 11bdd49e..00000000 --- a/src/main/java/org/ayfaar/app/spring/Model.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.ayfaar.app.spring; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Target({METHOD}) -@Retention(RUNTIME) -public @interface Model { - String[] keepProperties() default {}; -} diff --git a/src/main/java/org/ayfaar/app/spring/ModelMethodReturnValueHandler.java b/src/main/java/org/ayfaar/app/spring/ModelMethodReturnValueHandler.java deleted file mode 100644 index ac344d14..00000000 --- a/src/main/java/org/ayfaar/app/spring/ModelMethodReturnValueHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.ayfaar.app.spring; - -import org.springframework.core.MethodParameter; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; -import org.springframework.web.method.support.ModelAndViewContainer; - -import static org.ayfaar.app.utils.ValueObjectUtils.getModelMap; - -/** - * Decorator that detects a declared , and - * injects support if required - * @author y.lebid@spryflash.com - * - */ -public class ModelMethodReturnValueHandler implements HandlerMethodReturnValueHandler { - - private final HandlerMethodReturnValueHandler delegate; - - public ModelMethodReturnValueHandler(HandlerMethodReturnValueHandler delegate) - { - this.delegate = delegate; - } - - @Override - public boolean supportsReturnType(MethodParameter returnType) { - return returnType.getMethodAnnotation(Model.class) != null; - } - - @Override - public void handleReturnValue(Object returnValue, - MethodParameter returnType, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest) throws Exception { - - Model modelAnnotation = returnType.getMethodAnnotation(Model.class); - - returnValue = getModelMap(returnValue, true, modelAnnotation.keepProperties()); - - delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest); - } - -} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/spring/ModelViewSupportFactoryBean.java b/src/main/java/org/ayfaar/app/spring/ModelViewSupportFactoryBean.java deleted file mode 100644 index a75ab4aa..00000000 --- a/src/main/java/org/ayfaar/app/spring/ModelViewSupportFactoryBean.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.ayfaar.app.spring; - -import com.google.common.collect.Lists; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; -import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; -import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; - -import java.util.List; - -/** - * Modified Spring internal Return value handlers, and wires up a decorator - * to add support for @Model - * - * @author y.lebid@spryflash.com - * - */ -public class ModelViewSupportFactoryBean implements InitializingBean { - - @Autowired RequestMappingHandlerAdapter adapter; - - @Override - public void afterPropertiesSet() throws Exception { - HandlerMethodReturnValueHandlerComposite returnValueHandlers = adapter.getReturnValueHandlers(); - List handlers = Lists.newArrayList(returnValueHandlers.getHandlers()); - - ModelMethodReturnValueHandler modelHandler = null; - - for (HandlerMethodReturnValueHandler handler : handlers) { - if (handler instanceof RequestResponseBodyMethodProcessor) - { - modelHandler = new ModelMethodReturnValueHandler(handler); - break; - } - } - - handlers.add(0, modelHandler); -// handlers.add(0, new CsvMessageConverter()); - adapter.setReturnValueHandlers(handlers); - } - - -} diff --git a/src/main/java/org/ayfaar/app/spring/converter/json/CustomObjectMapper.java b/src/main/java/org/ayfaar/app/spring/converter/json/CustomObjectMapper.java deleted file mode 100644 index b5e15a46..00000000 --- a/src/main/java/org/ayfaar/app/spring/converter/json/CustomObjectMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.ayfaar.app.spring.converter.json; - -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.SerializationConfig; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import java.text.SimpleDateFormat; - -@Component("jacksonObjectMapper") -public class CustomObjectMapper extends ObjectMapper { - - @PostConstruct - public void afterPropertiesSet() throws Exception { - - SerializationConfig serialConfig = getSerializationConfig() - .withDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); - - this.setSerializationConfig(serialConfig); - } -} diff --git a/src/main/java/org/ayfaar/app/spring/handler/DefaultRestErrorResolver.java b/src/main/java/org/ayfaar/app/spring/handler/DefaultRestErrorResolver.java index 747bcef1..d457a92b 100644 --- a/src/main/java/org/ayfaar/app/spring/handler/DefaultRestErrorResolver.java +++ b/src/main/java/org/ayfaar/app/spring/handler/DefaultRestErrorResolver.java @@ -1,33 +1,59 @@ package org.ayfaar.app.spring.handler; +import org.ayfaar.app.event.QuietException; +import org.ayfaar.app.utils.exceptions.ConfirmationRequiredException; +import org.ayfaar.app.utils.exceptions.ExceptionCode; +import org.ayfaar.app.utils.exceptions.LogicalException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Component; import org.springframework.web.context.request.ServletWebRequest; +@Component public class DefaultRestErrorResolver implements RestErrorResolver { private static final Logger logger = LoggerFactory.getLogger(DefaultRestErrorResolver.class); +// @Autowired +// private ApplicationEventPublisher eventPublisher; @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Override public BusinessError resolveError(ServletWebRequest request, Object handler, Exception ex) { + if (ex instanceof LogicalException){ + String message = ex instanceof ConfirmationRequiredException + ? ((ConfirmationRequiredException) ex).action.getId().toString() + : ex.toString(); + return new BusinessError(((LogicalException) ex).getCode().name(), message, null); + } + + if (ex instanceof AccessDeniedException) { + return new BusinessError(ExceptionCode.ACCESS_DENIED.name()); + } + // ex.printStackTrace(System.out); - logger.error("Exception", ex); + if (!(ex instanceof QuietException)) { + logger.error("Exception", ex); + } if (ex instanceof NullPointerException) { String stackTrace = ""; for (StackTraceElement element : ex.getStackTrace()) { stackTrace += "\n" + element.toString(); } +// eventPublisher.publishEvent(new DefaultRestErrorEvent("Exception, UNDEFINED:",ex.toString() + "\n" + stackTrace)); return new BusinessError("UNDEFINED", ex.toString(), stackTrace); } Throwable mySQLIntegrityConstraintViolationException = findInChain("com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException", ex); if (mySQLIntegrityConstraintViolationException != null) { +// eventPublisher.publishEvent(new DefaultRestErrorEvent("Exception:",mySQLIntegrityConstraintViolationException.getMessage() + "\n" + ex.getMessage())); return new BusinessError(mySQLIntegrityConstraintViolationException.getMessage(), ex.getMessage()); } - + if (!(ex instanceof QuietException)) { +// eventPublisher.publishEvent(new DefaultRestErrorEvent("Exception", ex.toString())); + } return new BusinessError(ex.toString(), ex.getMessage()); } @@ -48,4 +74,5 @@ private Throwable findInChain(String className, Throwable ex) { } return null; } -} + +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/spring/handler/RestExceptionHandler.java b/src/main/java/org/ayfaar/app/spring/handler/RestExceptionHandler.java index c28e6c6a..b8f7ca4f 100644 --- a/src/main/java/org/ayfaar/app/spring/handler/RestExceptionHandler.java +++ b/src/main/java/org/ayfaar/app/spring/handler/RestExceptionHandler.java @@ -24,6 +24,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.servlet.ModelAndView; @@ -38,6 +39,7 @@ import java.util.Collections; import java.util.List; +@Component public class RestExceptionHandler extends AbstractHandlerExceptionResolver implements InitializingBean { private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class); diff --git a/src/main/java/org/ayfaar/app/spring/listeners/NotificationListener.java b/src/main/java/org/ayfaar/app/spring/listeners/NotificationListener.java new file mode 100644 index 00000000..fde24dc0 --- /dev/null +++ b/src/main/java/org/ayfaar/app/spring/listeners/NotificationListener.java @@ -0,0 +1,47 @@ +package org.ayfaar.app.spring.listeners; + + +import com.pushbullet.Builder; +import com.pushbullet.PushbulletClient; +import org.ayfaar.app.event.PushEvent; +import org.ayfaar.app.event.HasUrl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import static com.pushbullet.Builder.pushbullet; + +@Component +public class NotificationListener implements ApplicationListener { + + @Autowired + private PushbulletClient pushbulletClient; + @Value("${pushbullet.channel}") + private String channel; + @Value("${pushbullet.key}") + private String key; + @Autowired + private ApplicationContext ctx; + + @Override + public void onApplicationEvent(final PushEvent event) { + + if (ctx.getParent()!=null) return; // fix to avoid duplications + + if (key == null || key.isEmpty()) { + // todo пишем в лог что за ивент + return; + } + + new Thread(() -> { + final Builder.PushesBuilder pusher = pushbullet(pushbulletClient).pushes().channel(channel); + if(event instanceof HasUrl){ + pusher.link(event.getTitle(), event.getMessage(), ((HasUrl) event).getUrl()); + }else { + pusher.note(event.getTitle(), event.getMessage()); + } + }).start(); + } +} diff --git a/src/main/java/org/ayfaar/app/spring/listeners/TermsTaggingUpdateListener.java b/src/main/java/org/ayfaar/app/spring/listeners/TermsTaggingUpdateListener.java new file mode 100644 index 00000000..962c3064 --- /dev/null +++ b/src/main/java/org/ayfaar/app/spring/listeners/TermsTaggingUpdateListener.java @@ -0,0 +1,37 @@ +package org.ayfaar.app.spring.listeners; + +import org.ayfaar.app.event.PushEvent; +import org.ayfaar.app.event.TermPushEvent; +import org.ayfaar.app.event.TermUpdatedEvent; +import org.ayfaar.app.utils.TermsTaggingUpdater; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.stereotype.Component; + + +@Component +public class TermsTaggingUpdateListener implements ApplicationListener { + @Autowired TermsTaggingUpdater taggingUpdater; + @Autowired ApplicationContext ctx; + @Autowired AsyncTaskExecutor taskExecutor; + + @Override + public void onApplicationEvent(final PushEvent event) { + if (ctx.getParent() != null) return; // fix to avoid duplications + + if(event instanceof TermPushEvent) { + taskExecutor.submit(new Runnable() { + @Override + public void run() { + if (event instanceof TermUpdatedEvent && ((TermUpdatedEvent) event).morphAlias != null) { + taggingUpdater.updateSingle(((TermUpdatedEvent) event).morphAlias); + } else { + taggingUpdater.update(((TermPushEvent) event).getName()); + } + } + }); + } + } +} diff --git a/src/main/java/org/ayfaar/app/sync/ExportVideosWithoutCodesFormYoutubeChannel.java b/src/main/java/org/ayfaar/app/sync/ExportVideosWithoutCodesFormYoutubeChannel.java new file mode 100644 index 00000000..0227e2c4 --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/ExportVideosWithoutCodesFormYoutubeChannel.java @@ -0,0 +1,101 @@ +package org.ayfaar.app.sync; + +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.PlaylistItem; +import com.google.api.services.youtube.model.PlaylistItemContentDetails; +import com.google.api.services.youtube.model.PlaylistItemListResponse; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.VideoResource; +import org.ayfaar.app.services.GoogleSpreadsheetService; +import org.ayfaar.app.utils.GoogleService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.apache.commons.lang.StringUtils.isEmpty; + +@RestController +@RequestMapping("api/export-no-code-videos") +public class ExportVideosWithoutCodesFormYoutubeChannel { + + @Inject CommonDao commonDao; + @Inject GoogleSpreadsheetService spreadsheetService; + + @RequestMapping + public void export() throws IOException { + + final YouTube youtube = GoogleService.getYoutubeService(); + + final String uploadsPlaylistId = "UUbh7Mn8ri5PKx6mOridaHCA"; + + List allVideos = new ArrayList<>(); + + String pageToken = null; + do { + PlaylistItemListResponse sliceOfVideos = youtube.playlistItems() + .list("contentDetails") + .setMaxResults(50L) + .setPlaylistId(uploadsPlaylistId) + .setPageToken(pageToken) + .execute(); + allVideos.addAll(sliceOfVideos.getItems()); + pageToken = sliceOfVideos.getNextPageToken(); + } while (pageToken != null); + + List videosWithoutCodes = new ArrayList<>(); + + allVideos.stream() + .map(PlaylistItem::getContentDetails) + .map(PlaylistItemContentDetails::getVideoId) + .forEach(videoId -> { + final Optional opt = commonDao.getOpt(VideoResource.class, "id", videoId); + final VideoResource video = opt.get(); + if (!video.getOfficial()) { + video.setOfficial(true); + commonDao.save(video); + } + + if (isEmpty(video.getCode())) videosWithoutCodes.add(video); + }); + + GoogleSpreadsheetSynchronizer synchronizer = GoogleSpreadsheetSynchronizer.build(spreadsheetService, "1FQMsePNcMBKDibwe_BeqGcJnntXScx2YcxJmy8w_rGw") + .keyGetter(VideoSyncData::url) + .localDataLoader(() -> videosWithoutCodes.stream() + .map(v -> VideoSyncData.builder() + .name(v.getTitle()) + .url("https://youtu.be/" + v.getId()) + .build()) + .collect(Collectors.toList())) + .build(); + synchronizer.sync(); + } + + @Getter + @Setter + @Accessors(fluent = true) + @Builder + @ToString + private static class VideoSyncData implements SyncItem { + public String name; + public String url; + + @Override + public List toRaw() { + final ArrayList obj = new ArrayList<>(); + obj.add(name); + obj.add(url); + return obj; + } + } +} diff --git a/src/main/java/org/ayfaar/app/sync/GetVideosFormYoutube.java b/src/main/java/org/ayfaar/app/sync/GetVideosFormYoutube.java new file mode 100644 index 00000000..5162e011 --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/GetVideosFormYoutube.java @@ -0,0 +1,121 @@ +package org.ayfaar.app.sync; + +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.*; +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.VideoResource; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.utils.GoogleService; +import org.ayfaar.app.utils.Language; +import org.springframework.boot.logging.LogLevel; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.ayfaar.app.utils.GoogleService.codeVideoPatternRegExp; + +@Service +@Slf4j +public class GetVideosFormYoutube { + + @Inject CommonDao commonDao; + @Inject TopicService topicService; + @Inject EventPublisher publisher; + + @Scheduled(cron = "0 0 3 * * ?") // at 3 AM every day + public void synchronize() throws IOException { + + final YouTube youtube = GoogleService.getYoutubeService(); + + // https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=PlanetaAyfaar3&key=AIzaSyDZ2HuAgqxzjXCyzlkhN67pXcYtW_WuVLk + final String channelId = "UCbh7Mn8ri5PKx6mOridaHCA";// channel id for user PlanetaAyfaar3 + final String uploadsPlaylistId = "UUbh7Mn8ri5PKx6mOridaHCA"; + + List allVideos = new ArrayList<>(); + + String pageToken = null; + do { + PlaylistItemListResponse sliceOfVideos = youtube.playlistItems() + .list("contentDetails") + .setMaxResults(50L) + .setPlaylistId(uploadsPlaylistId) + .setPageToken(pageToken) + .execute(); + allVideos.addAll(sliceOfVideos.getItems()); + pageToken = sliceOfVideos.getNextPageToken(); + } while (pageToken != null); + + List addedVideos = new ArrayList<>(); + + allVideos.stream() + .map(PlaylistItem::getContentDetails) + .map(PlaylistItemContentDetails::getVideoId) + .forEach(videoId -> { + final Optional opt = commonDao.getOpt(VideoResource.class, "id", videoId); + if (!opt.isPresent()) { + VideoListResponse response; + try { + response = youtube.videos().list("snippet ").set("id", videoId).execute(); + } catch (IOException e) { + log.error("Error while getting data for youtube video id: " + videoId, e); + publisher.publishEvent(SysLogEvent.builder() + .source("GetVideosFormYoutube") + .message("Ошибка при получении данных с ютюба для видео " + videoId) + .level(LogLevel.ERROR) + .build()); + return; + } + if (response.getItems().size() != 1) { + log.error("Size of response not 1 for youtube video id: " + videoId); + publisher.publishEvent(SysLogEvent.builder() + .source("GetVideosFormYoutube") + .message("При получении данных с ютюба для видео " + videoId + " количество записей не равно 1, а равно " + response.getItems().size()) + .level(LogLevel.ERROR) + .build()); + return; + } + final VideoSnippet snippet = response.getItems().get(0).getSnippet(); + final VideoResource video = new VideoResource(videoId, Language.ru); + video.setTitle(snippet.getTitle()); + video.setPublishedAt(new Date(snippet.getPublishedAt().getValue())); + + snippet.getTags().forEach(tag -> { + Matcher matcher = Pattern.compile(codeVideoPatternRegExp).matcher(tag); + if (matcher.find()) { + video.setCode(matcher.group(0)); + } + }); + + commonDao.save(video); + + topicService.findOrCreate("нетегированные ответы", false, false).link(video); + + addedVideos.add(video); + } + }); + + if (addedVideos.size() > 0) { + String message = "Автоматически добавлены следующие видео ответы: "; + message += StreamEx.of(addedVideos) + .map(v -> String.format("%s", v.getTitle(), v.getUri())) + .joining(", "); + publisher.publishEvent(SysLogEvent.builder() + .source("GetVideosFormYoutube") + .message(message) + .level(LogLevel.INFO) + .build()); + } + } +} diff --git a/src/main/java/org/ayfaar/app/sync/GoogleSpreadsheetSynchronizer.java b/src/main/java/org/ayfaar/app/sync/GoogleSpreadsheetSynchronizer.java new file mode 100644 index 00000000..f5c51699 --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/GoogleSpreadsheetSynchronizer.java @@ -0,0 +1,130 @@ +package org.ayfaar.app.sync; + +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.services.GoogleSpreadsheetService; + +import java.io.IOException; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.springframework.util.StringUtils.isEmpty; + +@Slf4j +public class GoogleSpreadsheetSynchronizer { + private final HashMap> columnUpdaters; + private GoogleSpreadsheetService service; + private String spreadsheetId; + private Supplier> localDataLoader; + private Function keyGetter; + private boolean skipFirstRow; + + private GoogleSpreadsheetSynchronizer(GoogleSpreadsheetService service, String spreadsheetId) { + this.service = service; + this.spreadsheetId = spreadsheetId; + columnUpdaters = new HashMap<>(); + } + + public static Builder build(GoogleSpreadsheetService service, String spreadsheetId) { + return new Builder<>(service, spreadsheetId); + } + + public void sync() throws IOException { + final Collection localData = localDataLoader.get(); + final List> remoteData = service.read(spreadsheetId, "A:Z"); + if (!remoteData.isEmpty() && skipFirstRow) remoteData.remove(0); + + remoteToLocal(localData, remoteData); + localToRemote(localData, remoteData); + } + + private void localToRemote(Collection localData, List> remoteData) throws IOException { + final int[] index = {remoteData.size() + 1 + (skipFirstRow ? 1 : 0)}; + + final ArrayList localToRemoteData = new ArrayList<>(); + localData.forEach(localItem -> { + final boolean presentOnRemoteSide = remoteData.stream().anyMatch(remoteItem -> keyGetter.apply(localItem).equals(remoteItem.get(0))); + if (!presentOnRemoteSide) localToRemoteData.add(localItem); + }); + localToRemoteData.forEach(item -> log.debug("item to remote side: " + item)); + final Map> batchData = StreamEx.of(localToRemoteData).toMap(item -> index[0]++, SyncItem::toRaw); + service.write(spreadsheetId, batchData); + } + + private void remoteToLocal(Collection localData, List> remoteData) { + remoteData.forEach(remoteItem -> localData.stream() + .filter(localItem -> keyGetter.apply(localItem).equals(remoteItem.get(0))) + .forEach(localItem -> { + final List localRawItem = localItem.toRaw(); + for (int i = 1; i < localRawItem.size(); i++) { + if (remoteItem.size() <= i) continue; + String remoteValue = (String) remoteItem.get(i); + String localValue = (String) localRawItem.get(i); + if (!Objects.equals(localValue, remoteValue) && !isEmpty(localValue) && !isEmpty(remoteValue) && columnUpdaters.containsKey(i + 1)) { + String key = keyGetter.apply(localItem); + log.debug("Update from remote. key: {}, column: {}, local value: {}, remote value: {}", + key, i + 1, localValue, remoteValue); + columnUpdaters.get(i + 1).accept(key, remoteValue); + } + } + })); + } + + public void send(String key, int column, String value) throws IOException { + final List> remoteData = service.read(spreadsheetId, "A:Z"); + if (!remoteData.isEmpty() && skipFirstRow) remoteData.remove(0); + + for (int i = 0; i < remoteData.size(); i++) { + final List item = remoteData.get(i); + if (key.equals(item.get(0))) { + item.set(column - 1, value); + service.write(spreadsheetId, i + 1 + (skipFirstRow ? 1 : 0), item); + } + } + } + + + public static class Builder { + private final GoogleSpreadsheetSynchronizer synchronizer; + + private Builder(GoogleSpreadsheetService service, String spreadsheetId) { + synchronizer = new GoogleSpreadsheetSynchronizer<>(service, spreadsheetId); + } + + public Builder keyGetter(Function keyGetter) { + synchronizer.keyGetter = keyGetter; + return this; + } + + public Builder columnUpdater(int column, BiConsumer updater) { + synchronizer.columnUpdaters.put(column, updater); + return this; + } + + public GoogleSpreadsheetSynchronizer build() { + return synchronizer; + } + + public Builder localDataLoader(Supplier> localDataLoader) { + synchronizer.localDataLoader = localDataLoader; + return this; + } + + public Builder skipFirstRow() { + synchronizer.skipFirstRow = true; + return this; + } + + public Builder direction(SyncDirection direction) { + // todo + return this; + } + } + + public enum SyncDirection { + REMOTE_TO_LOCAL, LOCAL_TO_REMOTE, TWO_WAY + } + +} diff --git a/src/main/java/org/ayfaar/app/sync/RecordSynchronizer.java b/src/main/java/org/ayfaar/app/sync/RecordSynchronizer.java new file mode 100644 index 00000000..f813ffc6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/RecordSynchronizer.java @@ -0,0 +1,151 @@ +package org.ayfaar.app.sync; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.event.RecordRenamedEvent; +import org.ayfaar.app.model.Record; +import org.ayfaar.app.model.VideoResource; +import org.ayfaar.app.services.GoogleSpreadsheetService; +import org.ayfaar.app.services.record.RecordService; +import org.ayfaar.app.services.videoResource.VideoResourceService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.*; + +import static org.springframework.util.StringUtils.isEmpty; + +/** + * 1. Initial uploading + * 1.1. Get all record names + * 1.2. Put names to spreadsheet + * код ответа | упрощённое название | название для опытных | описание + * 2. Synchronization + * 2.1. Get all data from spreadsheet + * 2.2. Get all records data (names and descriptions) + * 2.3. On conflicts override local data by spreadsheet + * 2.4. All local changes translate to the spreadsheet + */ +@Service +@EnableScheduling +@Slf4j +public class RecordSynchronizer { + private final RecordService recordService; + private final VideoResourceService videoResourceService; + + private final GoogleSpreadsheetSynchronizer synchronizer; + + @Inject + public RecordSynchronizer(GoogleSpreadsheetService spreadsheetService, + RecordService recordService, + VideoResourceService videoResourceService, + @Value("${sync.records.spreadsheet-id}") String spreadsheetId) { + this.recordService = recordService; + this.videoResourceService = videoResourceService; + + synchronizer = GoogleSpreadsheetSynchronizer.build(spreadsheetService, spreadsheetId) + .keyGetter(RecordSyncData::code) + .skipFirstRow() +// .direction(TWO_WAY) + .columnUpdater(2, this::updateSimpleName) + .columnUpdater(3, this::updateOriginalName) + .columnUpdater(4, this::updateDescription) + .localDataLoader(this::dataLoader) + .build(); + } + + private Collection dataLoader() { + final List records = recordService.getAll(); + final List videos = videoResourceService.getAll(); + + // create data based on records + final List syncData = StreamEx.of(records) + .map(record -> RecordSyncData.builder() + .code(record.getCode()) + .originalName(record.getName()) + .description(record.getDescription()) + .build()) + .toList(); + + // add data based on videos + videos.stream().filter(v -> !isEmpty(v.getCode())).forEach(v -> { + final Optional match = syncData.stream().filter(i -> Objects.equals(i.code, v.getCode())).findFirst(); + if (match.isPresent()) { + match.get().simpleName(v.getTitle()); + } else { + syncData.add(RecordSyncData.builder() + .code(v.getCode()) + .simpleName(v.getTitle()) + .build()); + + } + }); + return syncData; + } + + @Scheduled(cron = "0 0 2 * * ?") // at 2 AM every day + public void synchronize() throws IOException { + synchronizer.sync(); + } + + @Async + @EventListener + public void onRecordRenamed(RecordRenamedEvent event) throws IOException { + final Record record = event.record; + synchronizer.send(record.getCode(), 3, record.getName()); + } + + private void updateSimpleName(String code, String name) { + log.info("updateSimpleName: update request for code: {}, new name: {}", code, name); + } + + private void updateOriginalName(String code, String name) { + log.info("updateOriginalName: update request for code: {}, new name: {}", code, name); + recordService.getByCode(code).ifPresent(record -> { + record.setName(name); + recordService.save(record); + }); + } + + private void updateDescription(String code, String description) { + log.info("updateDescription: update request for code: {}, new description: {}", code, description); + recordService.getByCode(code).ifPresent(record -> { + record.setDescription(description); + recordService.save(record); + }); + } + + @Getter @Setter + @Accessors(fluent = true) + @Builder + @ToString + private static class RecordSyncData implements SyncItem { + public String code; + + public String simpleName; + + public String originalName; + + public String description; + + public List toRaw() { + final ArrayList obj = new ArrayList<>(); + obj.add(code); + obj.add(simpleName != null ? simpleName : ""); + obj.add(originalName != null ? originalName : ""); + obj.add(description); + return obj; + } + } +} diff --git a/src/main/java/org/ayfaar/app/sync/SyncItem.java b/src/main/java/org/ayfaar/app/sync/SyncItem.java new file mode 100644 index 00000000..b0959f9b --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/SyncItem.java @@ -0,0 +1,7 @@ +package org.ayfaar.app.sync; + +import java.util.List; + +public interface SyncItem { + List toRaw(); +} diff --git a/src/main/java/org/ayfaar/app/sync/VocabularySynchronizer.java b/src/main/java/org/ayfaar/app/sync/VocabularySynchronizer.java new file mode 100644 index 00000000..caec4596 --- /dev/null +++ b/src/main/java/org/ayfaar/app/sync/VocabularySynchronizer.java @@ -0,0 +1,91 @@ +package org.ayfaar.app.sync; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.Term; +import org.ayfaar.app.services.GoogleSpreadsheetService; +import org.ayfaar.app.utils.TermService; +import org.springframework.boot.logging.LogLevel; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import static org.springframework.util.StringUtils.isEmpty; + +@Service +@Slf4j +@EnableScheduling +public class VocabularySynchronizer { + public static final String me = "Синхронизатор словаря"; + + @Inject TermService termService; + @Inject GoogleSpreadsheetService spreadsheetService; + @Inject EventPublisher publisher; + + @Scheduled(cron = "0 0 * * * *") // every hour + public void synchronize() throws IOException { + GoogleSpreadsheetSynchronizer synchronizer = GoogleSpreadsheetSynchronizer.build(spreadsheetService, "1h3Gy0x1-OvpznGvrugPBf7-Rqr9_MXtbj09AXrV9q2Q") + .keyGetter(VocabularySyncItem::term) + .skipFirstRow() + .localDataLoader(this::getLocalData) + .columnUpdater(2, this::updateShortDescription) + .build(); + synchronizer.sync(); + } + + private void updateShortDescription(String termName, String newShortDescription) { + final Optional termProviderOpt = termService.get(termName); + if (termProviderOpt.isPresent()) { + final Term term = termProviderOpt.get().getTerm(); + String oldShortDescription = term.getShortDescription(); + if (isEmpty(oldShortDescription)) oldShortDescription = "<пусто>"; + term.setShortDescription(newShortDescription); + termService.save(term); + publisher.publishEvent(new SysLogEvent(me, String.format("Обновлено короткое описание термина %s. Старый вариант: %s, новый: %s", termName, oldShortDescription, newShortDescription), LogLevel.INFO)); + } else { + // не возможная пока ситуация + publisher.publishEvent(new SysLogEvent(me, "Появился новый термин: " + termName, LogLevel.WARN)); + } + } + + private Collection getLocalData() { + return termService.getAll().stream() + .map(Map.Entry::getValue) + .map(TermService.TermProvider::getMainOrThis) + .distinct() + .map(term -> VocabularySyncItem.builder() + .term(term.getName()) + .shortDescription(term.getShortDescription().orElse(null)) + .build()) + .collect(Collectors.toList()); + } + + @Getter + @Setter + @Accessors(fluent = true) + @Builder + @ToString + private static class VocabularySyncItem implements SyncItem { + public String term; + public String shortDescription; + + @Override + public List toRaw() { + final ArrayList obj = new ArrayList<>(); + obj.add(term); + obj.add(shortDescription); + return obj; + } + } +} diff --git a/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteBot.java b/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteBot.java deleted file mode 100644 index acbeda32..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteBot.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.ayfaar.app.synchronization.evernote; - -import com.evernote.auth.EvernoteAuth; -import com.evernote.auth.EvernoteService; -import com.evernote.clients.ClientFactory; -import com.evernote.clients.NoteStoreClient; -import com.evernote.clients.UserStoreClient; -import com.evernote.edam.error.EDAMNotFoundException; -import com.evernote.edam.error.EDAMSystemException; -import com.evernote.edam.error.EDAMUserException; -import com.evernote.edam.notestore.NoteFilter; -import com.evernote.edam.notestore.NoteList; -import com.evernote.edam.type.Note; -import com.evernote.edam.type.NoteSortOrder; -import com.evernote.edam.type.Notebook; -import com.evernote.edam.type.Tag; -import com.evernote.thrift.TException; -import lombok.Data; -import org.apache.commons.lang3.StringEscapeUtils; -import org.ayfaar.app.model.Item; -import org.htmlcleaner.HtmlCleaner; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -import java.util.*; - -@Component -@Lazy -public class EvernoteBot { -// private static final String SANDBOX_AUTH_TOKEN = "S=s1:U=8ec6c:E=14df1be8cf4:C=1469a0d60f7:P=1cd:A=en-devtoken:V=2:H=95063e0650870375ceab5794def758ce"; - private static final String AUTH_TOKEN = "S=s2:U=23b8d:E=14e070668bd:C=146af553cc3:P=1cd:A=en-devtoken:V=2:H=45afea3eecc7db1ed47916383b5a6827"; - private static final String NOTEBOOK_NAME = "Интерактивная ИИ (автоматический импорт)"; - private static final String QUOTE_TAG_NAME = "цитата"; - private static final String LINK_TAG_NAME = "связь"; - public static final String LINK_EXIST_TAG_NAME = "ссылка существует"; - public static final String TERM_NOT_EXIST_TAG_NAME = "термин несуществует"; - private static final String ALLOWED_TAG_NAME = "проверенно"; - public static final String ITEM_NOT_EXIST_TAG_NAME = "пункт не найден"; - public static final String QUOTE_ALTERED_TAG_NAME = "цитата изменена"; -// public static final String UNDEFINED_TAG_TAG_NAME = "метка не определена"; - public static final String LACK_OF_TERMS_TAG_NAME = "недостаток терминов"; - public static final String NO_TERMS_TAG_NAME = "нет терминов"; - - private static final List ERROR_TAGS = Arrays.asList( - LINK_EXIST_TAG_NAME, TERM_NOT_EXIST_TAG_NAME, ITEM_NOT_EXIST_TAG_NAME, QUOTE_ALTERED_TAG_NAME, - LACK_OF_TERMS_TAG_NAME, NO_TERMS_TAG_NAME); - private static final List SERVICE_TAGS = Arrays.asList( - QUOTE_TAG_NAME, LINK_TAG_NAME, ALLOWED_TAG_NAME); - - - private UserStoreClient userStore; - private NoteStoreClient noteStore; - - public void init() throws Exception { - if (noteStore != null) return; - - // Set up the UserStore client and check that we can speak to the server - EvernoteAuth evernoteAuth = new EvernoteAuth(EvernoteService.PRODUCTION, AUTH_TOKEN); - ClientFactory factory = new ClientFactory(evernoteAuth); - userStore = factory.createUserStoreClient(); - - boolean versionOk = userStore.checkVersion("II App", - com.evernote.edam.userstore.Constants.EDAM_VERSION_MAJOR, - com.evernote.edam.userstore.Constants.EDAM_VERSION_MINOR); - if (!versionOk) { - System.err.println("Incompatible Evernote client protocol version"); - System.exit(1); - } - - // Set up the NoteStore client - noteStore = factory.createNoteStoreClient(); - } - - public List getExportNotes() throws Exception { - List exportNotes = new ArrayList(); - - Map tagsMap = new HashMap(); - - List notebooks = noteStore.listNotebooks(); - for (Notebook notebook : notebooks) { - if (!notebook.getName().equals(NOTEBOOK_NAME)) continue; - - NoteFilter filter = new NoteFilter(); - filter.setNotebookGuid(notebook.getGuid()); - filter.setOrder(NoteSortOrder.CREATED.getValue()); - filter.setAscending(true); - - NoteList noteList = noteStore.findNotes(filter, 0, 1000); - List notes = noteList.getNotes(); - - for (Note note : notes) { - if (note.getTagGuids() == null) continue; - - List tags = new ArrayList(); - for (String tagGuid : note.getTagGuids()) { - String tagName = tagsMap.get(tagGuid); - if (tagName == null) { - Tag tag = noteStore.getTag(tagGuid); - tagName = tag.getName(); - tagsMap.put(tagGuid, tagName); - } - tags.add(tagName); - } - if (tags.contains(QUOTE_TAG_NAME)) { - QuoteLink link = new QuoteLink(); - link.setAllowed(tags.contains(ALLOWED_TAG_NAME)); - boolean conflictedLink = false; - for (String tag : tags) { - if (ERROR_TAGS.contains(tag) && !link.getAllowed()) { - conflictedLink = true; - break; - } else if (SERVICE_TAGS.contains(tag) || ERROR_TAGS.contains(tag)) { - // skip - } else if (Item.isItemNumber(tag)) { - link.setItem(tag); - } else { - link.getTerms().add(tag); - } - } - if (conflictedLink) continue; // skip link on conflicted - if (link.getTerms().size() == 0) { - setTag(note.getGuid(), NO_TERMS_TAG_NAME); - continue; - } - - String text = noteStore.getNoteContent(note.getGuid()); - if (text != null && !text.isEmpty()) { - text = new HtmlCleaner().clean(text).getText().toString(); - text = StringEscapeUtils.unescapeHtml4(text); - text = text.trim(); - link.setQuote(text.isEmpty() ? null : text); - } - link.setGuid(note.getGuid()); - exportNotes.add(link); - } else if (tags.contains(LINK_TAG_NAME)) { - RelatedTerms related = new RelatedTerms(); - related.setAllowed(tags.contains(ALLOWED_TAG_NAME)); - related.setType(null); - related.setGuid(note.getGuid()); - - boolean conflictedLink = false; - for (String tag : tags) { - if (ERROR_TAGS.contains(tag) && !related.getAllowed()) { - conflictedLink = true; - break; - } else if (!SERVICE_TAGS.contains(tag) && !ERROR_TAGS.contains(tag)) { - related.getTerms().add(tag); - } - } - if (conflictedLink) continue; // skip link on conflicted - - related.getTerms().addAll(Arrays.asList(note.getTitle().split(" "))); - if (related.getTerms().size() == 0) { - setTag(note.getGuid(), NO_TERMS_TAG_NAME); - continue; - } - exportNotes.add(related); - } - - } - } - return exportNotes; - } - - public void removeNote(String guid) throws EDAMUserException, EDAMSystemException, TException, EDAMNotFoundException { - noteStore.deleteNote(guid); - } - - public void setTag(String guid, String tag) throws EDAMUserException, EDAMSystemException, TException, EDAMNotFoundException { - Note note = noteStore.getNote(guid, false, false, false, false); - note.addToTagNames(tag); - noteStore.updateNote(note); - } - - @Data - public class QuoteLink extends ExportNote { - private String item; - private Set terms = new HashSet(); - private String quote; - } - - @Data - public class RelatedTerms extends ExportNote { - private Byte type; - private List terms = new ArrayList(); - } - - @Data - public class ExportNote { - private String guid; - private Boolean allowed; - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteSync.java b/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteSync.java deleted file mode 100644 index 75ad8ca1..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/evernote/EvernoteSync.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.ayfaar.app.synchronization.evernote; - -import com.evernote.edam.error.EDAMNotFoundException; -import com.evernote.edam.error.EDAMSystemException; -import com.evernote.edam.error.EDAMUserException; -import com.evernote.thrift.TException; -import org.ayfaar.app.controllers.TermController; -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.dao.LinkDao; -import org.ayfaar.app.model.Item; -import org.ayfaar.app.model.Link; -import org.ayfaar.app.model.Term; -import org.ayfaar.app.spring.Model; -import org.ayfaar.app.utils.EmailNotifier; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -import java.util.List; - -import static org.ayfaar.app.utils.UriGenerator.generate; - -@Controller -@RequestMapping("evernote") -@EnableScheduling -public class EvernoteSync { - @Autowired EvernoteBot bot; - @Autowired LinkDao linkDao; - @Autowired CommonDao commonDao; - @Autowired TermController termController; - @Autowired EmailNotifier emailNotifier; - - @RequestMapping("sync") - @Scheduled(fixedDelay = 300000, initialDelay = 60000) // after each 5 min - @Model - public void sync() throws Exception { - bot.init(); - List exportNotes = bot.getExportNotes(); - - for (EvernoteBot.ExportNote exportNote : exportNotes) { - if (exportNote.getGuid() == null || exportNote.getGuid().isEmpty()) { - throw new RuntimeException("No GUID"); - } - if (exportNote instanceof EvernoteBot.QuoteLink) { - syncQuoteLink((EvernoteBot.QuoteLink) exportNote); - } - if (exportNote instanceof EvernoteBot.RelatedTerms) { - syncRelatedTerms((EvernoteBot.RelatedTerms) exportNote); - } - } - } - - private void syncRelatedTerms(EvernoteBot.RelatedTerms relatedTerms) throws Exception { - if (relatedTerms.getTerms().size() < 2) { - bot.setTag(relatedTerms.getGuid(), EvernoteBot.LACK_OF_TERMS_TAG_NAME); - return; - } - String mainTermName = relatedTerms.getTerms().get(0); - relatedTerms.getTerms().remove(0); - String mainTermUri = generate(Term.class, mainTermName); - - Term mainTerm = commonDao.get(Term.class, mainTermUri); - if (mainTerm == null) { - if (!relatedTerms.getAllowed()) { - bot.setTag(relatedTerms.getGuid(), EvernoteBot.TERM_NOT_EXIST_TAG_NAME); - return; - } - mainTerm = termController.add(mainTermName); - } - - for (String termName : relatedTerms.getTerms()) { - String termUri = generate(Term.class, termName); - Term term = commonDao.get(Term.class, termUri); - if (term == null) { - if (!relatedTerms.getAllowed()) { - bot.setTag(relatedTerms.getGuid(), EvernoteBot.TERM_NOT_EXIST_TAG_NAME); - return; - } - term = termController.add(termName); - } - Link link = linkDao.save(new Link(mainTerm, term, relatedTerms.getType())); - - emailNotifier.newLink(mainTermName, termName, link.getLinkId()); - } - bot.removeNote(relatedTerms.getGuid()); - } - - private void syncQuoteLink(EvernoteBot.QuoteLink potentialLink) throws EDAMUserException, EDAMSystemException, EDAMNotFoundException, TException { - String itemUri = generate(Item.class, potentialLink.getItem()); - - for (String termName : potentialLink.getTerms()) { - String termUri = generate(Term.class, termName); - - List existingLists = linkDao.getByUris(itemUri, termUri); - - if (existingLists.size() > 0 && !potentialLink.getAllowed()) { - bot.setTag(potentialLink.getGuid(), EvernoteBot.LINK_EXIST_TAG_NAME); - return; - } - - Term term = commonDao.get(Term.class, termUri); - if (term == null) { - if (!potentialLink.getAllowed()) { - bot.setTag(potentialLink.getGuid(), EvernoteBot.TERM_NOT_EXIST_TAG_NAME); - return; - } - term = termController.add(termName); - } - Item item = commonDao.get(Item.class, itemUri); - if (item == null) { - bot.setTag(potentialLink.getGuid(), EvernoteBot.ITEM_NOT_EXIST_TAG_NAME); - return; - } - if (potentialLink.getQuote() != null && !item.getContent().contains(potentialLink.getQuote())) { - bot.setTag(potentialLink.getGuid(), EvernoteBot.QUOTE_ALTERED_TAG_NAME); - return; - } - - Link link = linkDao.save(new Link(term, item, potentialLink.getQuote())); - - emailNotifier.newQuoteLink(termName, potentialLink.getItem(), - potentialLink.getQuote(), link.getLinkId()); - } - bot.removeNote(potentialLink.getGuid()); - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/CategorySync.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/CategorySync.java deleted file mode 100644 index 33abde89..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/CategorySync.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.controllers.ItemController; -import org.ayfaar.app.dao.CategoryDao; -import org.ayfaar.app.dao.ItemDao; -import org.ayfaar.app.model.Category; -import org.ayfaar.app.model.Item; -import org.ayfaar.app.utils.ParagraphHelper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.regex.Pattern; - -import static java.lang.String.format; -import static org.ayfaar.app.model.Category.PARAGRAPH_NAME; -import static org.ayfaar.app.model.Category.PARAGRAPH_SIGN; -import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; - -@Component -public class CategorySync extends EntitySynchronizer { - @Autowired MediaWikiBotHelper mediaWikiBotHelper; - @Autowired ParagraphHelper paragraphHelper; - @Autowired ItemDao itemDao; - @Autowired ItemSync itemSync; - @Autowired CategoryDao categoryDao; - - @Override - public void synchronize(Category category) throws Exception { - if (!category.isParagraph()) { - throw new RuntimeException("Should be paragraph"); - } - String articleName = SyncUtils.getArticleName(Category.class, category.getUri()); -// mediaWikiBotHelper.isSyncNeeded(articleName); - validateTitle(articleName); - - StringBuilder sb = new StringBuilder(); - - if (category.isParagraph()) { - sb.append(format("{{DISPLAYTITLE:%s %s}}\n", category.getName().replace("Параграф", "§"), category.getDescription())); - Item currentItem = itemDao.get(category.getStart()); - String itemNumber = currentItem.getNumber(); - String endNumber = null; - if (category.getEnd() != null) { - endNumber = itemDao.get(category.getEnd()).getNumber(); - } - do { - itemSync.scheduleSync(currentItem); - String itemId = SyncUtils.getArticleName(Item.class, currentItem.getUri()); - sb.append(format("[[%s|%s]]. {{:%s}}

\n", - itemId, - itemNumber, - itemId)); - itemNumber = ItemController.getNext(itemNumber); - currentItem = itemDao.getByNumber(itemNumber); - } while (endNumber != null && !itemNumber.equals(endNumber)); - - if (category.getNext() != null) { -// String next = getValueFromUri(Category.class, category.getNext()); - Category next = categoryDao.get(category.getNext()); - sb.append(String.format("[[%s|Следующий %s %s]]\n", - SyncUtils.getArticleName(Category.class, next.getUri()), - next.getName().replace(PARAGRAPH_NAME, PARAGRAPH_SIGN), - next.getDescription())); - } - } else { - if (category.getDescription() != null) { - sb.append(format("== %s ==\n", category.getDescription())); - } - } - - if (category.getParent() != null) { - Category parent = categoryDao.get(category.getParent()); - sb.append(String.format("\n[[%s|%s. %s]]", - SyncUtils.getArticleName(Category.class, parent.getUri()), - getValueFromUri(Category.class, parent.getUri()), - parent.getDescription())); - } - - mediaWikiBotHelper.saveArticle(articleName, sb.toString()); - } - - private static final Pattern INVALID_CHARS_PATTERN = - Pattern.compile("[#{}<>\\[\\]\\|]"); - - private void validateTitle(String title) throws Exception { - if (title == null || title.isEmpty()) { - throw new Exception("Title should not be empty"); - } - if (title.length() > 144) { - throw new Exception("Title length should be less then 144"); - } - // http://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(technical_restrictions) - if (INVALID_CHARS_PATTERN.matcher(title).find()) { - throw new Exception("Title should not contains # < > [ ] | { }"); - } - } - -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/EntitySynchronizer.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/EntitySynchronizer.java deleted file mode 100644 index da240b1f..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/EntitySynchronizer.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.model.UID; - -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -public abstract class EntitySynchronizer { - abstract void synchronize(E entity) throws Exception; - - protected Map scheduled = new LinkedHashMap(); - - public void scheduleSync(E uid) { - scheduled.put(uid.getUri(), uid); - } - - public void syncScheduled() throws Exception { - Set> syncPass = new HashSet>(scheduled.entrySet()); - scheduled.clear(); - for (Map.Entry entry : syncPass) { - synchronize(entry.getValue()); - } - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/ItemSync.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/ItemSync.java deleted file mode 100644 index d3b906c1..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/ItemSync.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.dao.CategoryDao; -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.model.Category; -import org.ayfaar.app.model.Item; -import org.ayfaar.app.utils.AliasesMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import static java.lang.String.format; -import static org.ayfaar.app.model.Category.PARAGRAPH_NAME; -import static org.ayfaar.app.model.Category.PARAGRAPH_SIGN; - -@Component -public class ItemSync extends EntitySynchronizer { - - @Autowired AliasesMap aliasesMap; - @Autowired CommonDao commonDao; - @Autowired CategoryDao categoryDao; - @Autowired MediaWikiBotHelper mediaWikiBotHelper; - @Autowired TermSync termSync; - - public void synchronize(Item item) throws Exception { - String content = item.getContent(); - Category paragraph = categoryDao.getForItem(item.getUri()); - - content = termSync.markTerms(content); - - if (item.getNext() != null || paragraph != null) { - content += "\n\n"; - if (item.getNext() != null) { - content += String.format("[[%s|Следующий пункт]] ", SyncUtils.getArticleName(Item.class, item.getNext())); - } - if (paragraph != null) { - String pName = SyncUtils.getArticleName(Category.class, paragraph); - content += format("[[%s|%s]]", pName, pName.replace(PARAGRAPH_NAME+":", PARAGRAPH_SIGN)); - } - content += "\n"; - } - - mediaWikiBotHelper.saveArticle(SyncUtils.getArticleName(Item.class, item.getUri()), content); - } - - - /*public void scheduleSync(Item item, Category paragraph) { - super.scheduleSync(item); - paragraphMap.put(item, paragraph); - }*/ -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/MediaWikiBotHelper.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/MediaWikiBotHelper.java deleted file mode 100644 index 5fb05ab5..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/MediaWikiBotHelper.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import net.sourceforge.jwbf.core.contentRep.SimpleArticle; -import net.sourceforge.jwbf.mediawiki.bots.MediaWikiBot; -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.model.SyncStatus; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; - -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.util.Date; -import java.util.List; - -@Component -public class MediaWikiBotHelper { - @Autowired CommonDao commonDao; - - private MediaWikiBot bot; - private PrintStream out; - - public MediaWikiBotHelper() throws UnsupportedEncodingException { - out = new PrintStream(System.out, true, "UTF-8"); - } - - public MediaWikiBot getBot() { - if (bot == null) { - bot = new MediaWikiBot("http://mediawiki/"); -// bot = new MediaWikiBot("http://direct.ayfaar.org/mediawiki/"); - bot.login("admin", "ayfaar"); - } - return bot; - } - - public void push() { - List toBeSync = commonDao.getList(SyncStatus.class, "synchronised", false); - toBeSync.addAll(commonDao.getList(SyncStatus.class, "synchronised", null)); - for (SyncStatus status : toBeSync) { - SimpleArticle article = new SimpleArticle(status.getArticleName()); - article.setText(status.getArticleContent()); - getBot().writeContent(article); - status.setSynchronised(true); - status.setSyncDate(new Date()); - commonDao.save(status); - out.println("[Pushed] " + article.getTitle()); - } - } - - public void saveArticle(String title, String text) { - Assert.hasLength(title); - Assert.hasLength(text); - SyncStatus status = commonDao.get(SyncStatus.class, "articleName", title); - if (status == null) { - status = new SyncStatus(); - status.setArticleName(title); - } - status.setSynchronised(false); - status.setArticleContent(text); - commonDao.save(status); - out.println("[Scheduled] " + title); - } - - public void isSyncNeeded(String articleName) { - - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/SyncUtils.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/SyncUtils.java deleted file mode 100644 index 7357d8bb..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/SyncUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.model.Category; -import org.ayfaar.app.model.Item; -import org.ayfaar.app.model.Term; -import org.ayfaar.app.model.UID; -import org.ayfaar.app.utils.UriGenerator; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class SyncUtils { - public static String getArticleName(Class objectClass, UID entity) { - return getArticleName(objectClass, entity.getUri()); - } - public static String getArticleName(Class objectClass, String uri) { - String name = UriGenerator.getValueFromUri(objectClass, uri); - if (Item.class.equals(objectClass)) { - name = "Пункт:"+name; - } - if (Category.class.equals(objectClass)) { - if (name.startsWith(Category.PARAGRAPH_NAME)) { - name = name.replace(" ", ":"); - } else { - name = TOCSync.NS_NAME+":"+name; - } - } - return name; - } - - @Autowired TermSync termSync; - @Autowired CategorySync categorySync; - @Autowired ItemSync itemSync; - - public void scheduleSync(UID entity) { - EntitySynchronizer synchronizer = null; - if (entity instanceof Term) { - synchronizer = termSync; - } else if (entity instanceof Item) { - synchronizer = itemSync; - } else if (entity instanceof Category) { - synchronizer = categorySync; - } - if (synchronizer != null) { - synchronizer.scheduleSync(entity); - } - } - - public void syncAllScheduled() throws Exception { - termSync.syncScheduled(); - itemSync.syncScheduled(); - categorySync.syncScheduled(); - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/Synchronizer.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/Synchronizer.java deleted file mode 100644 index 46d0110d..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/Synchronizer.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.dao.LinkDao; -import org.ayfaar.app.model.Category; -import org.ayfaar.app.model.Revision; -import org.hibernate.envers.RevisionType; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -@Component -public class Synchronizer { - @Autowired CommonDao commonDao; - @Autowired CategorySync categorySync; - @Autowired LinkDao linkDao; - -// @PostConstruct - public void synchronize() throws Exception { - List newRevisions = commonDao.getList(Revision.class, "wikiSynchronized", false); - - List entities = new ArrayList(); - - for (Revision revision : newRevisions) { - entities.addAll(commonDao.findAuditEntities(revision.getId(), RevisionType.ADD)); -// entities.addAll(auditReader.findEntities(revision.getId(), RevisionType.MOD)); - } - - for (Object entity : entities) { - if (entity instanceof Category) { - Category cat = (Category) entity; - if (cat.getStart() != null && cat.getEnd() != null) { - categorySync.synchronize(cat); - } - } - } - - } - -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/TOCSync.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/TOCSync.java deleted file mode 100644 index 35baf18c..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/TOCSync.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import lombok.extern.slf4j.Slf4j; -import org.ayfaar.app.dao.CategoryDao; -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.model.Category; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; - -import static java.lang.String.format; -import static org.ayfaar.app.model.Category.PARAGRAPH_SIGN; -import static org.ayfaar.app.synchronization.mediawiki.SyncUtils.getArticleName; -import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; - -@Component -@Slf4j -public class TOCSync { - @Autowired CommonDao commonDao; - @Autowired CategoryDao categoryDao; - @Autowired MediaWikiBotHelper mediaWikiBotHelper; - @Autowired CategorySync categorySync; - - public static final String NS_NAME = "Содержание"; - - private StringBuilder sb; - private PrintStream out; - private Queue queue; - - public TOCSync() throws UnsupportedEncodingException { - out = new PrintStream(System.out, true, "UTF-8"); - } - - public void synchronize() throws UnsupportedEncodingException { - sb = new StringBuilder(); - List topCategories = categoryDao.getTopLevel(); - List toms = new ArrayList(); - - for (Category category : topCategories) { - for (Category child : getChildren(category)) { - setLine(child, ""); - toms.add(child); - } - } -// mediaWikiBotHelper.saveArticle(NS_NAME+":Тома", sb.toString()); - - queue = new ArrayDeque(toms); - Category category = queue.poll(); - while (category != null) { - sb = new StringBuilder(); - for (Category child : getChildren(category)) { - recursiveCompute(child, "="); - } - if (category.getParent() != null) { - Category parent = categoryDao.get(category.getParent()); - sb.append(format("\n[[%s|%s%s]]", - getArticleName(Category.class, parent.getUri()), - getValueFromUri(Category.class, parent.getUri()), - parent.getDescription() != null ? ". "+parent.getDescription() : "")); - } - mediaWikiBotHelper.saveArticle(NS_NAME + ":" + category.getName(), sb.toString()); - category = queue.poll(); - } - - } - - private void recursiveCompute(Category category, String depth) { - if (!category.isParagraph()) { - queue.add(category); - } - setLine(category, depth); - for (Category child : getChildren(category)) { - recursiveCompute(child, depth+"="); - } - } - - private void setLine(Category category, String depth) { - String name = category.getName(); - String label = name; - int i = label.lastIndexOf("/"); - if (i > 0) { - label = label.substring(i+2); - } - - String line; - if (category.isParagraph()) { - categorySync.scheduleSync(category); - label = PARAGRAPH_SIGN + "" + name.substring(name.lastIndexOf(".")+1); - line = format("* [[%s|%s. %s]]\n", - getArticleName(Category.class, category), - label, - category.getDescription()); - } else { - line = format("%s[[%s:%s|%s%s]]%s\n", - depth.isEmpty() ? "" : depth+" ", - NS_NAME, - name, - label, - category.getDescription() != null ? ". "+category.getDescription() : "", - depth.isEmpty() ? "" : " "+depth); - } - if (category.isTom()) { - line = "* "+line; - } - sb.append(line); - } - - private List getChildren(Category parent) { - List children = new ArrayList(); - if (parent.getStart() != null) { - Category child = categoryDao.get(parent.getStart()); - if (child != null) { - children.add(child); - while (child.getNext() != null) { - child = categoryDao.get(child.getNext()); - if (!child.getParent().equals(parent.getUri())) - break; - children.add(child); - } - } - } - return children; - } -} diff --git a/src/main/java/org/ayfaar/app/synchronization/mediawiki/TermSync.java b/src/main/java/org/ayfaar/app/synchronization/mediawiki/TermSync.java deleted file mode 100644 index ecb507c6..00000000 --- a/src/main/java/org/ayfaar/app/synchronization/mediawiki/TermSync.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.ayfaar.app.synchronization.mediawiki; - -import org.ayfaar.app.controllers.TermController; -import org.ayfaar.app.dao.LinkDao; -import org.ayfaar.app.model.Link; -import org.ayfaar.app.model.Term; -import org.ayfaar.app.model.UID; -import org.ayfaar.app.utils.AliasesMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.ui.ModelMap; - -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.lang.String.format; -import static java.util.regex.Pattern.*; -import static org.ayfaar.app.synchronization.mediawiki.SyncUtils.getArticleName; - -@Component -public class TermSync extends EntitySynchronizer { - @Autowired - MediaWikiBotHelper mediaWikiBotHelper; - @Autowired - TermController termController; - @Autowired - AliasesMap aliasesMap; - @Autowired - LinkDao linkDao; - @Autowired SyncUtils syncUtils; - - private Set alreadySync = new HashSet(); - - @Override - public void synchronize(Term term) throws Exception { - - if (alreadySync.contains(term.getUri())) { - return; - } - - // может быть аббравиатурой или сокращением или кодом - Term prime = (Term) linkDao.getPrimeForAlias(term.getUri()); - if (prime != null) { - saveRedirect(term, prime); - scheduleSync(prime); - return; - } - - /*Link _link = linkDao.getForAbbreviationOrAliasOrCode(term.getUri()); - if (_link != null && _link.getUid1() instanceof Term) { - Term mainTerm = (Term) _link.getUid1(); - saveRedirect(term, mainTerm); - synchronize(mainTerm); - return; - }*/ - - StringBuilder sb = new StringBuilder(); - - if (term.getDescription() != null && !term.getDescription().isEmpty()) { - sb.append(format("=== %s ===\n", term.getDescription())); - } - - // QUOTES - for (Link link : linkDao.getRelatedWithQuote(term.getUri())) { - ModelMap map = new ModelMap(); - UID source = link.getUid1().getUri().equals(term.getUri()) - ? link.getUid2() - : link.getUid1(); - String text = link.getQuote(); - text = markTerms(text); - sb.append(format("{{Quote|text=%s|sign=[[%s]]}}\n", text, getArticleName(source.getClass(), source.getUri()))); - syncUtils.scheduleSync(source); - } - - Set aliases = new LinkedHashSet(); - for (Link link : linkDao.getAliases(term.getUri())) { - aliases.add(link.getUid2()); - } - - Set related = new LinkedHashSet(); - for (Link link : linkDao.getRelated(term.getUri())) { - if (link.getQuote() == null) { - if (link.getUid1().getUri().equals(term.getUri())) { - related.add(link.getUid2()); - } else { - related.add(link.getUid1()); - } - } - } - if (aliases.size() > 0) { - sb.append(format("== Сокращения или синонимы ==\n")); - for (UID uid : aliases) { - sb.append(format("[[%s]], ", getArticleName(uid.getClass(), uid.getUri()))); - syncUtils.scheduleSync(uid); - } - sb.delete(sb.length()-2, sb.length()); - sb.append("\n"); - } - if (related.size() > 0) { - sb.append(format("== Связан с ==\n")); - for (UID uid : related) { - sb.append(format("[[%s]], ", getArticleName(uid.getClass(), uid.getUri()))); - syncUtils.scheduleSync(uid); - } - sb.delete(sb.length()-2, sb.length()); - } - - mediaWikiBotHelper.saveArticle(term.getName(), sb.length() == 0 ? "Ожидается наполнение" : sb.toString()); - alreadySync.add(term.getUri()); - } - - private void saveRedirect(Term fromTerm, Term toTerm) { - mediaWikiBotHelper.saveArticle(fromTerm.getName(), format("#redirect [[%s]]", toTerm.getName())); - alreadySync.add(fromTerm.getUri()); - } - - public String markTerms(String content) { - String result = content; - for (Map.Entry entry : aliasesMap.entrySet()) { - String key = entry.getKey(); - Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|\\-])|^)(" + key - + ")(([^A-Za-zА-Яа-я0-9Ёё\\]\\|])|$)", UNICODE_CHARACTER_CLASS | UNICODE_CASE | CASE_INSENSITIVE); - Matcher contentMatcher = pattern.matcher(content); - if (contentMatcher.find()) { - Matcher matcher = pattern.matcher(result); - if (matcher.find()) { - scheduleSync(entry.getValue().getTerm()); - String articleName = getArticleName(Term.class, entry.getValue().getUri()); - String found = contentMatcher.group(3); - String charBefore = contentMatcher.group(2) != null ? contentMatcher.group(2) : ""; - String charAfter = contentMatcher.group(5) != null ? contentMatcher.group(5) : ""; - String articleReplacer = articleName.equals(found) - ? articleName - : format("%s|%s", articleName, found); - String fullReplacer = format("%s[[%s]]%s", - charBefore, - articleReplacer, - charAfter - ); - result = matcher.replaceAll(fullReplacer); - } - content = contentMatcher.replaceAll(""); - } - } - return result; - } - - -} diff --git a/src/main/java/org/ayfaar/app/translation/GoogleSpreadsheetTranslator.java b/src/main/java/org/ayfaar/app/translation/GoogleSpreadsheetTranslator.java new file mode 100644 index 00000000..1016eecb --- /dev/null +++ b/src/main/java/org/ayfaar/app/translation/GoogleSpreadsheetTranslator.java @@ -0,0 +1,79 @@ +package org.ayfaar.app.translation; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.services.GoogleSpreadsheetService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.logging.LogLevel; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +@Component +public class GoogleSpreadsheetTranslator { + private final String spreadsheetId; + private final String range = "A:B"; + private final GoogleSpreadsheetService googleSpreadsheetService; + private final EventPublisher publisher; + + @Autowired + public GoogleSpreadsheetTranslator(GoogleSpreadsheetService service, + @Value("${translation.spreadsheet-id}") String spreadsheetId, + EventPublisher publisher) { + this.spreadsheetId = spreadsheetId; + this.googleSpreadsheetService = service; + this.publisher = publisher; + } + + public Stream read() { + List> values = new ArrayList<>(); + try { + values = googleSpreadsheetService.read(spreadsheetId, range); + } catch (IOException e) { + // dispatch syslog event + log.error("Can't read translation from range {}", range, e); + publisher.publishEvent(new SysLogEvent(this, "Can't read translation from range " + range + ". " + + e.getMessage(), LogLevel.ERROR)); + } + + List result = new ArrayList<>(); + for (List row : values) { + if (row.isEmpty()) { + continue; + } + TranslationItem translationItem = new TranslationItem(); + translationItem.setRowNumber(Optional.of(values.lastIndexOf(row) + 1)); + translationItem.setOrigin(((String) row.get(0)).trim()); + if (row.size() > 1) { + translationItem.setTranslation(((String) row.get(1)).trim()); + } + result.add(translationItem); + } + + return result.stream(); + } + + public Integer write(Stream translationItems) { + Map> batchData = new HashMap<>(); + translationItems.forEach(t -> + batchData.put(t.getRowNumber().get(), Stream.of(t.getOrigin(), t.getTranslation()).collect(Collectors.toList())) + ); + + Integer updatedRows = -1; + try { + updatedRows = googleSpreadsheetService.write(spreadsheetId, batchData); + } catch (IOException e) { + // dispatch syslog event + log.error("Can't write translations", e); + publisher.publishEvent(new SysLogEvent(this, "Can't write translations. " + e.getMessage(), LogLevel.ERROR)); + } + + return updatedRows; + } +} diff --git a/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizer.java b/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizer.java new file mode 100644 index 00000000..783a11fd --- /dev/null +++ b/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizer.java @@ -0,0 +1,119 @@ +package org.ayfaar.app.translation; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.Translation; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.services.translations.TranslationService; +import org.ayfaar.app.utils.Language; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.logging.LogLevel; +import org.springframework.stereotype.Service; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.ayfaar.app.services.moderation.Action.SYSLOG_TRANSLATION_NEW; +import static org.ayfaar.app.services.moderation.Action.SYSLOG_TRANSLATION_UPDATE; +import static org.slf4j.helpers.MessageFormatter.arrayFormat; + +@Slf4j +@Service +public class TopicTranslationSynchronizer { + private static final String NO_UPDATES = "Синхронизация: без обновлений"; + private static final String PREFIX_NEW_TRANSLATION = "Добавлен перевод для: "; + private static final String PREFIX_UPDATED_TRANSLATION = "Обновлён перевод для: "; + + private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss"); + private TopicService topicService; + private GoogleSpreadsheetTranslator googleSpreadsheetTranslator; + private TranslationComparator translationComparator; + private TranslationService translationService; + private final EventPublisher publisher; + + @Autowired + public TopicTranslationSynchronizer(TopicService topicService, + GoogleSpreadsheetTranslator translator, + TranslationComparator comparator, + TranslationService translationService, + EventPublisher publisher) { + this.topicService = topicService; + this.googleSpreadsheetTranslator = translator; + this.translationComparator = comparator; + this.translationService = translationService; + this.publisher = publisher; + } + + public void synchronize() { + log.info("Topic translation sync started {}", dateFormat.format(new Date())); + + // get data + List itemsTopicList = topicService.getAllNames().stream().map(TranslationItem::new).collect(Collectors.toList()); + Supplier> itemsTopicSupplier = itemsTopicList::stream; + List itemsGoogleList = googleSpreadsheetTranslator.read().collect(Collectors.toList()); + Supplier> itemsGoogleSupplier = itemsGoogleList::stream; + List itemsTranslationList = translationService.getAllAsTranslationItem().collect(Collectors.toList()); + Supplier> itemsTranslationSupplier = itemsTranslationList::stream; + // topic -> spreadsheet + googleSpreadsheetTranslator.write(translationComparator.getNotUploadedOrigins(itemsTopicSupplier.get(), itemsGoogleSupplier.get())); + // spreadsheet -> translation + Stream notDownloadedTranslations = + translationComparator.getNotDownloadedTranslations(itemsGoogleSupplier.get(), itemsTranslationSupplier.get()); + notDownloadedTranslations = translationComparator.removeIfNotInTopics(itemsTopicSupplier.get(), notDownloadedTranslations); + List notDownloadedTranslationsList = notDownloadedTranslations.collect(Collectors.toList()); + Supplier> notDownloadedTranslationsSupplier = notDownloadedTranslationsList::stream; + notDownloadedTranslationsSupplier.get() + .map(item -> new Translation(item.getOrigin(), item.getTranslation(), Language.en)) + .forEach(translationService::save); + + log.info("Topic translation sync finished {}", dateFormat.format(new Date())); + logTranslationInfoToDb(notDownloadedTranslationsList, itemsTranslationSupplier.get()); + } + + private void logTranslationInfoToDb(List notDownloadedTranslationsList, Stream itemsTranslated) { + List itemTranslatedList = itemsTranslated.collect(Collectors.toList()); + final AtomicReference messageUpdateAtomic = new AtomicReference<>(""); + final AtomicReference messageNewAtomic = new AtomicReference<>(""); + notDownloadedTranslationsList.stream() + .forEach(notDownloaded -> { + Optional foundTranslation = itemTranslatedList.stream() + .filter(t -> notDownloaded.getOrigin().equals(t.getOrigin()) && !t.getTranslation().isEmpty()) + .findAny(); + if (foundTranslation.isPresent()) { + messageUpdateAtomic.set(messageUpdateAtomic.get() + + arrayFormat(SYSLOG_TRANSLATION_UPDATE.message, new Object[]{notDownloaded.getOrigin(), + foundTranslation.get().getTranslation(), notDownloaded.getTranslation()}) + .getMessage()); + } else { + messageNewAtomic.set(messageNewAtomic.get() + + arrayFormat(SYSLOG_TRANSLATION_NEW.message, new Object[]{notDownloaded.getOrigin(), + notDownloaded.getTranslation()}).getMessage()); + } + }); + String messageNewTranslation = messageNewAtomic.get(); + String messageUpdatedTranslation = messageUpdateAtomic.get(); + String message = StringUtils.EMPTY; + if (StringUtils.isNotEmpty(messageNewTranslation)) { + messageNewTranslation = messageNewTranslation.substring(0, messageNewTranslation.length()-2); + messageNewTranslation = PREFIX_NEW_TRANSLATION + messageNewTranslation; + message = messageNewTranslation + ". "; + } + if (StringUtils.isNotEmpty(messageUpdatedTranslation)) { + messageUpdatedTranslation = messageUpdatedTranslation.substring(0, messageUpdatedTranslation.length()-2); + messageUpdatedTranslation = PREFIX_UPDATED_TRANSLATION + messageUpdatedTranslation; + message = message + messageUpdatedTranslation + "."; + } + if (!StringUtils.isEmpty(message)) { +// message = NO_UPDATES; + publisher.publishEvent(new SysLogEvent(this, message, LogLevel.INFO)); + } + } +} diff --git a/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizerScheduler.java b/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizerScheduler.java new file mode 100644 index 00000000..6bf9c56a --- /dev/null +++ b/src/main/java/org/ayfaar/app/translation/TopicTranslationSynchronizerScheduler.java @@ -0,0 +1,27 @@ +package org.ayfaar.app.translation; + +import org.ayfaar.app.services.translations.TranslationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@EnableScheduling +@Service +@Profile("!dev") +public class TopicTranslationSynchronizerScheduler { + TopicTranslationSynchronizer topicTranslationSynchronizer; + + @Autowired + public TopicTranslationSynchronizerScheduler(TopicTranslationSynchronizer topicTranslationSynchronizer) { + this.topicTranslationSynchronizer = topicTranslationSynchronizer; + } + + private TranslationService translationService; + + @Scheduled(cron = "0 0 1 * * ?") // at 1 AM every day + public void synchronize() { + topicTranslationSynchronizer.synchronize(); + } +} diff --git a/src/main/java/org/ayfaar/app/translation/TranslationComparator.java b/src/main/java/org/ayfaar/app/translation/TranslationComparator.java new file mode 100644 index 00000000..c05574b1 --- /dev/null +++ b/src/main/java/org/ayfaar/app/translation/TranslationComparator.java @@ -0,0 +1,81 @@ +package org.ayfaar.app.translation; + +import org.ayfaar.app.event.EventPublisher; +import org.ayfaar.app.event.SysLogEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.logging.LogLevel; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public class TranslationComparator { + private final EventPublisher publisher; + + @Autowired + public TranslationComparator(EventPublisher publisher) { + this.publisher = publisher; + } + + public Stream getNotUploadedOrigins(Stream originStream, Stream translatedStream) { + List fromGoogle = translatedStream.collect(Collectors.toList()); + final AtomicInteger lastRowNumber = new AtomicInteger(fromGoogle.stream() + .reduce((a, b) -> b).orElse(new TranslationItem(Optional.of(0))).getRowNumber().get() + 1); + return originStream + .flatMap(originItem -> fromGoogle.parallelStream() + .anyMatch(translatedItem -> originItem.getOrigin().equalsIgnoreCase(translatedItem.getOrigin())) + ? Stream.empty() : Stream.of(originItem)) + .peek(item -> item.setRowNumber(Optional.of(lastRowNumber.getAndIncrement()))); + } + + public Stream getNotDownloadedTranslations(Stream translatedItemsGoogle, + Stream translatedItemsDB) { + List fromDB = translatedItemsDB.collect(Collectors.toList()); + return translatedItemsGoogle + .filter(t -> !t.getTranslation().isEmpty()) + .flatMap(itemGoogle -> { + AtomicBoolean originSynced = new AtomicBoolean(false); + Optional syncedOriginNonSyncedTranslation = fromDB.parallelStream() + .filter(itemDB -> { + if (originSynced.get()) { + return false; + } + if (itemDB.getOrigin().equalsIgnoreCase(itemGoogle.getOrigin())) { + originSynced.set(true); + return !itemDB.getTranslation().equalsIgnoreCase(itemGoogle.getTranslation()); + } + return false; + }).findAny(); + return syncedOriginNonSyncedTranslation.isPresent() || !originSynced.get() + ? Stream.of(itemGoogle) : Stream.empty(); + }); + } + + public Stream removeIfNotInTopics(Stream originItems, Stream translatedItemsGoogle) { + List originAsList = originItems.collect(Collectors.toList()); + List notInOrigins = new ArrayList<>(); + List resultList = translatedItemsGoogle + .flatMap(itemGoogle -> { + boolean isPresent = originAsList.parallelStream().anyMatch(itemOrigin -> itemGoogle.getOrigin().equalsIgnoreCase(itemOrigin.getOrigin())); + if (isPresent) { + return Stream.of(itemGoogle); + } else { + notInOrigins.add(itemGoogle); + return Stream.empty(); + } + }).collect(Collectors.toList()); + + if (!notInOrigins.isEmpty()) { + publisher.publishEvent(new SysLogEvent(this, "Записи из таблици переводов не отражённая в системе: " + + notInOrigins.toString(), LogLevel.WARN)); // Items in Google Spreadsheet doesn't exist in Topic table + } + + return resultList.stream(); + } +} diff --git a/src/main/java/org/ayfaar/app/translation/TranslationItem.java b/src/main/java/org/ayfaar/app/translation/TranslationItem.java new file mode 100644 index 00000000..4d9a6d72 --- /dev/null +++ b/src/main/java/org/ayfaar/app/translation/TranslationItem.java @@ -0,0 +1,50 @@ +package org.ayfaar.app.translation; + +import lombok.*; + +import java.util.Optional; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@ToString +public class TranslationItem { + private Optional rowNumber = Optional.empty(); + private String origin = ""; + private String translation = ""; + + public TranslationItem(String origin) { + this.origin = origin; + } + + public TranslationItem(Optional rowNumber) { + this.rowNumber = rowNumber; + } + + public TranslationItem(String origin, String translation) { + this.origin = origin; + this.translation = translation; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TranslationItem item = (TranslationItem) o; + + if (rowNumber != null ? !rowNumber.equals(item.rowNumber) : item.rowNumber != null) return false; + if (origin != null ? !origin.equals(item.origin) : item.origin != null) return false; + return !(translation != null ? !translation.equals(item.translation) : item.translation != null); + + } + + @Override + public int hashCode() { + int result = rowNumber != null ? rowNumber.hashCode() : 0; + result = 31 * result + (origin != null ? origin.hashCode() : 0); + result = 31 * result + (translation != null ? translation.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/AdvanceComparator.java b/src/main/java/org/ayfaar/app/utils/AdvanceComparator.java new file mode 100644 index 00000000..549f371f --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/AdvanceComparator.java @@ -0,0 +1,95 @@ +package org.ayfaar.app.utils; + +import java.util.Comparator; + +public class AdvanceComparator implements Comparator { + public static final AdvanceComparator INSTANCE = new AdvanceComparator(); + + @Override + public int compare(String o1, String o2) { + if (o2 == null || o1 == null) { + return 0; + } + + int lengthFirstStr = o1.length(); + int lengthSecondStr = o2.length(); + + int index1 = 0; + int index2 = 0; + + //пока не достигнуты концы строк + while (index1 < lengthFirstStr && index2 < lengthSecondStr) { + char ch1 = o1.charAt(index1); + char ch2 = o2.charAt(index2); + + char[] space1 = new char[lengthFirstStr]; + char[] space2 = new char[lengthSecondStr]; + + int loc1 = 0; + int loc2 = 0; + + //строку o1 считываем посимвольно в space1 до тех пор, пока не будет достигнут конец строки o1 + //или пока не закончится непрерывная последовательность из символов (не цифр) или последовательность из цифр + do { + space1[loc1++] = ch1; + index1++; + + if (index1 < lengthFirstStr) { + ch1 = o1.charAt(index1); + } else { + break; + } + } while (Character.isDigit(ch1) == Character.isDigit(space1[0])); + + //строку o2 считываем посимвольно в space2 до тех пор, пока не будет достигнут конец строки o2 + //или пока не закончится непрерывная последовательность из символов (не цифр) или последовательность из цифр + do { + space2[loc2++] = ch2; + index2++; + + if (index2 < lengthSecondStr) { + ch2 = o2.charAt(index2); + } else { + break; + } + } while (Character.isDigit(ch2) == Character.isDigit(space2[0])); + + //Из массивов символов получаем сроки для дальнейшего сравнения. + //Это буду последовательности либо из цифр, либо из нецифровых символов. + String str1 = new String(space1); + String str2 = new String(space2); + + int result; + + //если нецифровые строки равны и следующие за ними символы в строках o1 и o2 цифры + if (index1 < lengthFirstStr && index2 < lengthSecondStr && str1.substring(0, index1).equalsIgnoreCase(str2.substring(0, index2)) && Character.isDigit(o1.charAt(index1)) && Character.isDigit(o2.charAt(index2))) { + + //найдем индекс последнего цифрового символа в последовательности в строке o1, увеличенный на 1 + int index11 = index1; + while ((index11 < lengthFirstStr) && (Character.isDigit(o1.charAt(index11)))) { + index11++; + } + + //найдем индекс последнего цифрового символа в последовательности в строке o2, увеличенный на 1 + int index21 = index2; + while ((index21 < lengthSecondStr) && (Character.isDigit(o2.charAt(index21)))){ + index21++; + } + + //получим числовые значения в строках и сравним их + Integer firstNumberToCompare = Integer.parseInt(o1.substring(index1, index11)); + Integer secondNumberToCompare = Integer.parseInt(o2.substring(index2, index21)); + result = firstNumberToCompare.compareTo(secondNumberToCompare); + } else { + result = str1.compareTo(str2); + } + + //Если сравниваемые последовательности не равны, то вернем результат сравнения. + //В противном случае продолжим сравнивать o1 и o2 в новом цикле. + if (result != 0) { + return result; + } + } + return lengthFirstStr - lengthSecondStr; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/AliasesMap.java b/src/main/java/org/ayfaar/app/utils/AliasesMap.java deleted file mode 100644 index 65845347..00000000 --- a/src/main/java/org/ayfaar/app/utils/AliasesMap.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.ayfaar.app.utils; - -import org.ayfaar.app.dao.CommonDao; -import org.ayfaar.app.dao.LinkDao; -import org.ayfaar.app.dao.TermDao; -import org.ayfaar.app.model.Term; -import org.ayfaar.app.model.TermMorph; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static java.util.Collections.sort; -import static java.util.regex.Pattern.compile; - -@Component -@Lazy -public class AliasesMap extends LinkedHashMap { - @Autowired TermDao termDao; - @Autowired LinkDao linkDao; - @Autowired CommonDao commonDao; - - private List allTerms; - private List allTermMorphs; - private Map proxyMap; - - @PostConstruct - private void load() { - clear(); - allTerms = termDao.getAll(); - allTermMorphs = commonDao.getAll(TermMorph.class); - proxyMap = new HashMap(); - Map tmpMap = new HashMap(); - - for (Term term : allTerms) { - Proxy proxy = proxyMap.get(term.getUri()); - if (proxy == null) { - proxy = new Proxy(term); - proxyMap.put(term.getUri(), proxy); - } - tmpMap.put(term.getName(), proxy); - } - for (TermMorph termMorph : allTermMorphs) { - Proxy proxy = proxyMap.get(termMorph.getTermUri()); - if (proxy == null) { - proxy = new Proxy(termMorph.getTermUri()); - proxyMap.put(termMorph.getTermUri(), proxy); - } - tmpMap.put(termMorph.getName(), proxy); - } - - List> entries = - new ArrayList>(tmpMap.entrySet()); - - sort(entries, new Comparator>() { - @Override - public int compare(Map.Entry o1, Map.Entry o2) { - int i = new Integer(o2.getKey().length()).compareTo(o1.getKey().length()); - if (i == 0) { - i = o1.getKey().compareTo(o2.getKey()); - } - return i; - } - }); - - for (Map.Entry entry : entries) { - put(entry.getKey(), entry.getValue()); - } - } - - public void reload() { - load(); - } - - public Proxy put(String alias, Term term) { - return put(alias, new Proxy(term)); - } - - @Override - public Proxy put(String key, Proxy value) { - return super.put(key.toLowerCase(), value); - } - - public Proxy get(String key) { - return super.get(key.toLowerCase()); - } - - public Term getTerm(String name) { - Proxy proxy = get(name); - if (proxy != null) { - return proxy.getTerm(); - } else { - return null; - } - } - - public class Proxy { - private String uri; - private Term term; - - public Proxy(Term term) { - this.term = term; - uri = term.getUri(); - } - - public Proxy(String uri) { - this.uri = uri; - } - - public Term getTerm() { - if (term == null) { - /*Link link = linkDao.getPrimeForAlias(term.getUri()); - if (link != null) { - prime = (Term) link.getUid1(); - } else { - prime = term; - }*/ - term = termDao.get(uri); - } - return term; - } - - public String getUri() { - return uri; - } - } - - public List getAllTerms() { - return allTerms; - } - - public List findTermsInside(String content) { - Set contains = new HashSet(); - content = content.toLowerCase(); - - for (Map.Entry entry : entrySet()) { - String key = entry.getKey().toLowerCase(); - Matcher matcher = compile("((" + RegExpUtils.W + ")|^)" + key - + "((" + RegExpUtils.W + ")|$)", Pattern.UNICODE_CHARACTER_CLASS) - .matcher(content); - if (matcher.find()) { - contains.add(entry.getValue().getTerm()); - content = content.replaceAll(key, ""); - } - } - - List sorted = new ArrayList(contains); - sort(sorted, new Comparator() { - @Override - public int compare(Term o1, Term o2) { - return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase()); - } - }); - return sorted; - } -} diff --git a/src/main/java/org/ayfaar/app/utils/CollectionUtils.java b/src/main/java/org/ayfaar/app/utils/CollectionUtils.java new file mode 100644 index 00000000..23c87c2b --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/CollectionUtils.java @@ -0,0 +1,15 @@ +package org.ayfaar.app.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class CollectionUtils { + public static List transform(Collection c, Transformer t) { + List result = new ArrayList(c.size()); + for (I item : c) { + result.add(t.transform(item)); + } + return result; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/ContentsService.java b/src/main/java/org/ayfaar/app/utils/ContentsService.java new file mode 100644 index 00000000..4aae1387 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/ContentsService.java @@ -0,0 +1,65 @@ +package org.ayfaar.app.utils; + + +import one.util.streamex.StreamEx; +import org.ayfaar.app.model.Category; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface ContentsService { + Optional get(String name); + Optional getCategory(String name); + Optional getParagraph(String name); + Optional getByItemNumber(String number); + List descriptionContains(List searchQueries); + void reload(); + + Optional getByUri(String uri); + + StreamEx getAllCategories(); + + StreamEx getAllParagraphs(); + + Map getAllUriNames(); + Map getAllUriDescription(); + + interface CategoryProvider extends ContentsProvider { + Category getCategory(); + String getParentUri(); +// boolean isParagraph(); + boolean isTom(); + boolean isCikl(); + boolean isContentsRoot(); + List children(); + String extractCategoryName(); + CategoryProvider getParent(); + + + Optional getPrevious(); + + Optional next(); + } + + interface ParagraphProvider extends ContentsProvider { + Optional previous(); + Optional parent(); + + String from(); + String to(); + } + + interface ContentsProvider { + String description(); + Optional previousUri(); + Optional nextUri(); + String code(); + String uri(); + String startItemNumber(); + String name(); + Optional next(); + List parents(); + String path(); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/ContentsServiceImpl.java b/src/main/java/org/ayfaar/app/utils/ContentsServiceImpl.java new file mode 100644 index 00000000..9bba8c9c --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/ContentsServiceImpl.java @@ -0,0 +1,428 @@ +package org.ayfaar.app.utils; + + +import lombok.Getter; +import lombok.experimental.Accessors; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.CategoryDao; +import org.ayfaar.app.model.Category; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.model.ItemsRange; +import org.ayfaar.app.services.itemRange.ItemRangeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; + +@Component +public class ContentsServiceImpl implements ContentsService { + private static final Logger logger = LoggerFactory.getLogger(ContentsService.class); + + @Inject CategoryDao categoryDao; + @Inject ItemRangeService itemRangeService; + + private Map categoryMap; + private Map paragraphMap; +// private List paragraphs; + + @PostConstruct + public void load() { + logger.info("Category map loading..."); + categoryMap = new HashMap<>(); + paragraphMap = new HashMap<>(); + + List categories = categoryDao.getAll(); + for(Category category : categories) { + CategoryProvider provider = new CategoryProviderImpl(category); + categoryMap.put(category.getName(), provider); + } + paragraphMap = StreamEx.of(itemRangeService.getWithCategories()) + .sortedBy(ItemsRange::getFrom) + .toMap(ItemsRange::getCode, Paragraph::new); + logger.info("Category map loading finish"); + } + + @Getter @Accessors(chain = true) + public class Paragraph implements ParagraphProvider { + private ItemsRange itemsRange; + private Double start; + private Double end; + + private Paragraph(ItemsRange itemsRange) { + this.itemsRange = itemsRange; + this.start = convertItemNumber(itemsRange.getFrom()); + this.end = convertItemNumber(itemsRange.getTo()); + } + + @Override + public String description() { + return name(); + } + + @Override + public Optional previousUri() { + return previous().isPresent() ? Optional.of(previous().get().uri()) : Optional.empty(); + } + + @Override + public Optional nextUri() { + return next().isPresent() ? Optional.of(next().get().uri()) : Optional.empty(); + } + + @Override + public String code() { + return itemsRange.getCode(); + } + + @Override + public String uri() { + return itemsRange.getUri(); + } + + @Override + public String startItemNumber() { + return from(); + } + + @Override + public String name() { + return itemsRange.getDescription(); + } + + @Override + public Optional next() { + final CategoryProvider category = getCategoryByUri(itemsRange.getCategory()) + .orElseThrow(() -> new RuntimeException("Category "+itemsRange.getCategory()+" not found in cache")); + final Iterator iterator = category.children().iterator(); + while (iterator.hasNext()) { + ContentsProvider child = iterator.next(); + if (child == this) return iterator.hasNext() ? Optional.of((Paragraph) iterator.next()) : Optional.empty(); + } + return Optional.empty(); + } + + @Override + public Optional previous() { + final CategoryProvider category = getCategoryByUri(itemsRange.getCategory()) + .orElseThrow(() -> new RuntimeException("Category "+itemsRange.getCategory()+" not found in cache")); + final ListIterator iterator = category.children().listIterator(); + while (iterator.hasPrevious()) { + ContentsProvider child = iterator.previous(); + if (child == this) return iterator.hasPrevious() ? Optional.of((Paragraph) iterator.previous()) : Optional.empty(); + } + return Optional.empty(); + } + + @Override + public Optional parent() { + return getCategoryByUri(itemsRange.getCategory()); + } + + @Override + public List parents() { + if (!parent().isPresent()) return Collections.emptyList(); + List parents = new LinkedList<>(); + parents.addAll(parent().get().parents()); + parents.add(parent().get()); + return parents; + } + + @Override + public String path() { + return parent().isPresent() ? label() + " / " + parent().get().path() : label(); + } + + public String label() { + return from() + "-" + to(); + } + + @Override + public String from() { + return itemsRange.getFrom(); + } + + @Override + public String to() { + return itemsRange.getTo(); + } + } + + @Override + public Optional get(String name) { + return Optional.ofNullable(categoryMap.containsKey(name) ? categoryMap.get(name) : paragraphMap.get(name)); + } + + @Override + public Optional getCategory(String name) { + return Optional.ofNullable(categoryMap.get(name)); + } + + @Override + public Optional getParagraph(String code) { + return Optional.ofNullable(paragraphMap.get(code)); + } + + @Override + public Optional getByItemNumber(String number) { + double itemNumber = convertItemNumber(UriGenerator.generate(Item.class, number)); + return StreamEx.of(paragraphMap.values()).findFirst(o -> { + Paragraph p = (Paragraph) o; + if(itemNumber >= p.start && itemNumber <= p.end) { + return true; + } else if(p.end == 0.0 && itemNumber == p.start){ + return true; + } + return false; + }); + } + + @Override + public List descriptionContains(List searchQueries) { + ListIterator iterator = searchQueries.listIterator(); + + String regexp = ""; + while (iterator.hasNext()) { + String q = iterator.next(); + regexp += RegExpUtils.buildWordContainsRegExp(q); + if (iterator.hasNext()) regexp += "|"; + } + +// final Map rateMap = new HashMap(); + + Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + List foundCategories = new ArrayList<>(); + for (CategoryProvider provider : categoryMap.values()) { + if (provider.description() == null || provider.description().isEmpty()) continue; + Matcher matcher = pattern.matcher(provider.description()); + if (matcher.find()) { + foundCategories.add(provider); +// rateMap.put(provider, matcher.start()); + } + } + + return foundCategories; + } + + @Override + public void reload() { + load(); + } + + public class CategoryProviderImpl implements CategoryProvider { + private Category category; + + public CategoryProviderImpl(Category category) { + this.category = category; + } + + @Override + public Category getCategory() { + return category; + } + + @Override + public String uri() { + return category.getUri(); + } + + @Override + public String name() { + return category.getName(); + } + + @Override + public String getParentUri() { + return category.getParent(); + } + + @Override + public String description() { + return category.getDescription(); + } + + @Override + public Optional next() { + return nextUri().isPresent() ? getCategoryByUri(nextUri().get()) : Optional.empty(); + } + + @Override + public boolean isTom() { + return category.getName().indexOf(Category.TOM_NAME) == 0; + } + + @Override + public boolean isCikl() { + return category.getName().equals("БДК") || category.getName().equals("Основы"); + } + + @Override + public boolean isContentsRoot() { + return category.getName().equals("Содержание"); + } + + @Override + public List children() { + List children = new ArrayList<>(); + if (category.getStart() != null) { + CategoryProvider child = categoryMap.get(getValueFromUri(Category.class, category.getStart())); + if (child != null) { + children.add(child); + while (child.next().isPresent()) { + child = child.next().get(); + if (!child.getParentUri().equals(uri())) + break; + children.add(child); + } + } + } + final List paragraphs = paragraphs() + .filter(p -> p.itemsRange.getCategory().equals(uri())) + .sortedByDouble(Paragraph::getStart) + .toList(); + children.addAll(paragraphs); + return children; + } + + @Override + public CategoryProvider getParent() { + return getParentUri() != null ? categoryMap.get(getValueFromUri(Category.class, getParentUri())) : null; + } + + @Override + public List parents() { + return getParents(getValueFromUri(Category.class, uri())); + } + + private List getParents(String name) { + List parents = new ArrayList(); + CategoryProvider parent = categoryMap.get(name).getParent(); + + if(parent != null) { + parents.addAll(getParents(getValueFromUri(Category.class, parent.uri()))); + parents.add(parent); + } + return parents; + } + + @Override + public String extractCategoryName() { + String[] split = uri().split("/|:"); + return split[split.length-1].trim(); + } + + @Override + public String path() { + String path = ""; + List chain = new ArrayList<>(parents()); + chain.add(0, this); + ListIterator iterator = chain.listIterator(chain.size()); + while (iterator.hasPrevious()) { + CategoryProvider provider = iterator.previous(); + if (provider.isContentsRoot() || provider.isCikl()) continue; +// if (provider.isParagraph()) path += "§"; + path += provider.extractCategoryName(); + if (iterator.hasPrevious()) path += " / "; + } + return path; + } + + @Override + public String code() { + return category.getName(); + } + + @Override + public Optional previousUri() { + for (CategoryProvider provider : categoryMap.values()) + if (category.getUri().equals(provider.getCategory().getNext())) return Optional.of(provider.uri()); + return Optional.empty(); + } + + @Override + public Optional nextUri() { + return Optional.ofNullable(category.getNext()); + } + + @Override + public String startItemNumber() { + return getStartItemNumberOfChildren(children()); + } + + private String getStartItemNumberOfChildren(List categories) { + if (categories.isEmpty()) return null; + ContentsProvider firstCat = categories.get(0); + return firstCat instanceof CategoryProvider + ? getStartItemNumberOfChildren(((CategoryProvider) firstCat).children()) + : null; + } + + @Override + public Optional getPrevious() { + return previousUri().isPresent() ? getByUri(previousUri().get()) : Optional.empty(); + } + } + + private StreamEx paragraphs() { + return StreamEx.of(paragraphMap.values()).map(p -> (Paragraph) p); + } + + @Override + public Optional getByUri(String uri) { + if (uri == null) return Optional.empty(); + final Optional catOpt = getCategoryByUri(uri); + return catOpt.isPresent() ? catOpt : getParagraphByUri(uri); + } + + public Optional getCategoryByUri(String uri) { + final CategoryProvider categoryProvider = categoryMap.get(UriGenerator.getValueFromUri(Category.class, uri)); + return Optional.ofNullable(categoryProvider); + } + + public Optional getParagraphByUri(String uri) { + final ParagraphProvider paragraphProvider = paragraphMap.get(UriGenerator.getValueFromUri(ItemsRange.class, uri)); + return Optional.ofNullable(paragraphProvider); + } + + private static Double convertItemNumber(String value) { + return value != null ? Double.parseDouble(getValueFromUri(Item.class, value)) : 0.0; + } + + @Override + public StreamEx getAllCategories(){ + return StreamEx.of(categoryMap.values()); + } + + @Override + public StreamEx getAllParagraphs() { + return paragraphs(); + } + + @Override + public Map getAllUriNames(){ + final Map map = categoryMap.values().stream() + .collect(Collectors.toMap(categoryProvider -> categoryProvider.getCategory().getUri(), + categoryProvider -> categoryProvider.getCategory().getName())); + + paragraphMap.values().forEach(p -> map.put(p.uri(), p.name())); + return map; + } + + @Override + public Map getAllUriDescription() { + final Map map = categoryMap.values().stream() + .filter(categoryProvider -> categoryProvider.getCategory().getDescription() != null) + .collect(Collectors.toMap(categoryProvider -> categoryProvider.getCategory().getUri(), + categoryProvider -> categoryProvider.getCategory().getDescription())); + + return map; + } +} + diff --git a/src/main/java/org/ayfaar/app/utils/CurrentUserProvider.java b/src/main/java/org/ayfaar/app/utils/CurrentUserProvider.java new file mode 100644 index 00000000..6f7500cd --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/CurrentUserProvider.java @@ -0,0 +1,27 @@ +package org.ayfaar.app.utils; + +import org.ayfaar.app.model.User; +import org.ayfaar.app.services.moderation.UserRole; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import javax.inject.Provider; +import java.util.Optional; + +@Component +public class CurrentUserProvider implements Provider> { + public Optional get(){ + if (SecurityContextHolder.getContext() == null || SecurityContextHolder.getContext().getAuthentication() == null) return Optional.empty(); + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return principal instanceof User ? Optional.of((User) principal) : Optional.empty(); + } + + public UserRole getCurrentAccessLevel() { +// return UserRole.ROLE_ADMIN; + return get().isPresent() ? get().get().getRole() : UserRole.ROLE_ANONYMOUS; + } + /*@Override + public Optional get() { + return AuthController.get(); + }*/ +} diff --git a/src/main/java/org/ayfaar/app/utils/EmailNotifier.java b/src/main/java/org/ayfaar/app/utils/EmailNotifier.java index 0ac016f8..f1bf75e8 100644 --- a/src/main/java/org/ayfaar/app/utils/EmailNotifier.java +++ b/src/main/java/org/ayfaar/app/utils/EmailNotifier.java @@ -1,5 +1,7 @@ package org.ayfaar.app.utils; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.model.Term; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -12,6 +14,8 @@ public class EmailNotifier { private static final String FROM = "ii@ayfaar.org"; private static final String TO = "sllouyssgort@gmail.com"; + + @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired JavaMailSender mailSender; public void newQuoteLink(String termName, String itemNumber, String quote, Integer linkId) { @@ -26,7 +30,7 @@ public void newQuoteLink(String termName, String itemNumber, String quote, Integ } catch (MessagingException e) { e.printStackTrace(); } - mailSender.send(helper.getMimeMessage()); + send(helper.getMimeMessage()); } public void newLink(String term, String alias, Integer linkId) { @@ -37,30 +41,42 @@ public void newLink(String term, String alias, Integer linkId) { helper.setTo(TO); helper.setSubject("Создана связь (" + term + " + " + alias + ")"); helper.setText("link id: " + linkId - + " " + getRemoveLink(linkId) + + " удалить связь " + getRemoveLink(linkId) + "\nhttp://ii.ayfaar.org/#" + term + "\nhttp://ii.ayfaar.org/#" + alias); } catch (MessagingException e) { e.printStackTrace(); } - mailSender.send(helper.getMimeMessage()); + send(helper.getMimeMessage()); } private String getRemoveLink(Integer linkId) { - return "удалить ссылку"; + return "http://ii.ayfaar.org/api/link/remove/" + linkId; } - public void rate(String kind, String query, String uri) { + public void rate(Term term, Item item, String quote, Integer linkId) { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage); try { helper.setFrom(FROM); helper.setTo(TO); - helper.setSubject("Рейтинг "+kind+" (" + query + " + " + uri + ")"); - helper.setText(""); + helper.setSubject("Связь через поиск (" + term.getName() + " + " + item.getNumber() + ")"); + helper.setText(quote + + (linkId == null ? "\n\nНе создана по причине возможной дубликации" + : "удалить связь "+getRemoveLink(linkId)) + ); } catch (MessagingException e) { e.printStackTrace(); } - mailSender.send(helper.getMimeMessage()); + send(helper.getMimeMessage()); + } + + private void send(final MimeMessage message) { + new Thread(new Runnable() { + @Override + public void run() { + mailSender.send(message); + } + }); } } diff --git a/src/main/java/org/ayfaar/app/utils/GoogleService.java b/src/main/java/org/ayfaar/app/utils/GoogleService.java new file mode 100644 index 00000000..a5851bf4 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/GoogleService.java @@ -0,0 +1,333 @@ +package org.ayfaar.app.utils; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.batch.BatchRequest; +import com.google.api.client.googleapis.batch.json.JsonBatchCallback; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.media.MediaHttpUploader; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.DriveScopes; +import com.google.api.services.drive.model.File; +import com.google.api.services.drive.model.Permission; +import com.google.api.services.sheets.v4.Sheets; +import com.google.api.services.sheets.v4.SheetsScopes; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.YouTubeScopes; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.client.RestTemplate; + +import javax.inject.Inject; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Arrays.asList; + +@Slf4j +@Component +public class GoogleService { + private static Sheets sheetsService; + private static YouTube youtubeService; + + @Value("${google.api.key}") private String API_KEY; + @Value("${drive-dir}") private String driveDir; + + private static final String APPLICATION_NAME = "ii"; + // https://developers.google.com/identity/protocols/OAuth2ServiceAccount + private static final String ACCOUNT_PRIVATE_KEY = "/account-private-key-google-api-devstarter.json"; + + private static HttpTransport httpTransport; + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + private static Drive drive; + + private final ResourceLoader resourceLoader; + private final RestTemplate restTemplate; + + private final String DocOrImageInfoUrl = "https://www.googleapis.com/drive/v2/files/{id}?key={key}"; + private final String forGetCodeOfVideoUrl = "https://www.googleapis.com/youtube/v3/videos?key={API_KEY}&fields=items(snippet(tags))&part=snippet&id={video_id}"; + public static final String codeVideoPatternRegExp = "^\\d{4}-\\d{2}-\\d{2}(_\\d{1,2})?([a-z])?([-_][km])?$"; + + /** Global instance of the HTTP transport. */ + private static HttpTransport HTTP_TRANSPORT; + static { + try { + HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + } catch (Throwable t) { + log.error(t.getMessage(), t); + } + } + + @Inject + public GoogleService(ResourceLoader resourceLoader, RestTemplate restTemplate) { + this.resourceLoader = resourceLoader; + this.restTemplate = restTemplate; + } + + public VideoInfo getVideoInfo(String id) { + final Map response = restTemplate.getForObject("https://content.googleapis.com/youtube/v3/videos?part={part}&id={id}&key={key}", + Map.class, "snippet", id, API_KEY); + //noinspection unchecked + final List items = (List) response.get("items"); + if (items.isEmpty()) throw new RuntimeException("Video private or removed"); + final Map snippet = (Map) items.get(0).get("snippet"); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + try { + return new VideoInfo((String) snippet.get("title"), dateFormat.parse((String) snippet.get("publishedAt"))); + } catch (ParseException e) { + throw new RuntimeException("Video date parsing error", e); + } + } + + public static String extractVideoIdFromYoutubeUrl(String url) { + //https://www.youtube.com/watch?v=044VwC_uptU + Matcher matcher = Pattern.compile("^https?://www\\.youtube\\.com/watch\\?v=([^&]+)").matcher(url); + if (matcher.find()) { + return matcher.group(1); + } else { + // https://youtu.be/1I1cy6z-FgY + matcher = Pattern.compile("^https?://youtu.be/(.*)$").matcher(url); + if (matcher.find()) { + return matcher.group(1); + } + } + throw new RuntimeException("Cannot resolve video id"); + } + + @SuppressWarnings("unchecked") + public Optional getCodeVideoFromYoutube(String id){ + String code = null; + final Map response = restTemplate.getForObject(forGetCodeOfVideoUrl,Map.class, API_KEY, id); + final List items = (List) response.get("items"); + + if (items.isEmpty()) return Optional.empty(); + final Map snippet = (Map) items.get(0).get("snippet"); + List tags = (List)snippet.get("tags"); + for (String tag : tags) { + //"\\d{4}-\\d{2}-\\d{2}(_\\d{1,2})?([-_][km])?" + Matcher matcher = Pattern.compile(codeVideoPatternRegExp).matcher(tag); + if (matcher.find()) { + code = matcher.group(0); + log.info("Code for video with id = " + id + ": " + code); + } + } + return Optional.ofNullable(code); + } + + public static String extractDocIdFromUrl(String url) { + //https://docs.google.com/document/d/1iWY8qI5Qn1V_90VpzfhFDTyAcNgati9u6sTv-A-gWQg/edit?usp=sharing + //https://drive.google.com/file/d/0BwGttgSD-WcTbTJFbWplN1hwcFU/view?usp=sharing + return extractIdFromUrl(url); + } + + public DocInfo getDocInfo(String id) { + final DocInfo doc = restTemplate.getForObject(DocOrImageInfoUrl, DocInfo.class, id, API_KEY); + Assert.notNull(doc.title); + return doc; + } + + public static String extractImageIdFromUrl(String url) { + return extractIdFromUrl(url); + } + + public ImageInfo getImageInfo(String id) { + final ImageInfo imageInfo = restTemplate.getForObject(DocOrImageInfoUrl, ImageInfo.class, id, API_KEY); + Assert.notNull(imageInfo.title); + return imageInfo; + } + + private static String extractIdFromUrl(String url) { + Matcher matcher = Pattern.compile("^https?://(docs|drive)\\.google\\.com/(document|file)/d/([^/]+)").matcher(url); + if (matcher.find()) { + return matcher.group(3); + } + throw new RuntimeException("Cannot resolve id"); + } + + public File uploadToGoogleDrive(String url, String title) throws IOException { + Resource resource = resourceLoader.getResource("file:" + driveDir); + if (!resource.exists()) { + throw new RuntimeException("Error locating Google Drive dir "+ driveDir); + } + + try { + httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Google Drive initialization error", e); + } + // authorization + Credential credential = authorize(DriveScopes.DRIVE); + // set up the global Drive instance + drive = new Drive.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build(); + // run command + return uploadFile(false, url, title); + } + + /** Uploads a file using either resumable or direct media upload. */ + private static File uploadFile(boolean useDirectUpload, String url, String title) { + + InputStream data; + try { + data = new URL(url).openStream(); + } catch (IOException e) { + log.warn("Url {} not accessible",url); + throw new RuntimeException("Url not accessible"); + } + + File fileMetadata = new File(); + fileMetadata.setTitle(title); + + InputStreamContent mediaContent = new InputStreamContent("", new BufferedInputStream(data)); + + Drive.Files.Insert insert; + try { + insert = drive.files().insert(fileMetadata, mediaContent); + } catch (IOException e) { + throw new RuntimeException("Google Drive file inserting error", e); + } + + MediaHttpUploader uploader = insert.getMediaHttpUploader(); + uploader.setDirectUploadEnabled(useDirectUpload); + log.info("Uploading {}...", title); + File execute; + try { + execute = insert.execute(); + } catch (IOException e) { + throw new RuntimeException("Google Drive insert execution error", e); + } + sharedAccess(execute.getId()); + log.info("Done"); + return execute; + } + + private static void sharedAccess(String id){ + JsonBatchCallback callback = new JsonBatchCallback() { + @Override + public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) throws IOException { + throw new RuntimeException(e.getMessage()); + } + + @Override + public void onSuccess(Permission permission, HttpHeaders responseHeaders)throws IOException { + log.info("Permission ID: " + permission.getId()); + } + }; + BatchRequest batch = drive.batch(); + Permission userPermission = new Permission() + .setType("anyone") + .setRole("reader"); + try { + drive.permissions().insert(id, userPermission) + .setFields("id") + .queue(batch, callback); + + batch.execute(); + } catch (IOException e) { + throw new RuntimeException(e); + } + +// Permission domainPermission = new Permission() //возможно понадобиться в дальнейшем +// .setType("domain") +// .setRole("reader") +// .setDomain("ii.ayfaar.org"); +// drive.permissions().insert(fileId, domainPermission) +// .setFields("id") +// .queue(batch, callback); + + + } + + /** + * Creates an authorized Credential object. + * @return an authorized Credential object. + * @throws IOException + * @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount + */ + private static Credential authorize(String... scopes) throws IOException { + GoogleCredential credential = null; + try { + credential = GoogleCredential + .fromStream(GoogleService.class.getResourceAsStream(ACCOUNT_PRIVATE_KEY)) + .createScoped(asList(scopes)); + } catch (IOException e) { + log.error("Can't create Google credential", e); + } + + return credential; + } + + /** + * Build and return an authorized Sheets API client service. + * @return an authorized Sheets API client service + */ + public static Sheets getSheetsService() throws IOException { + if (sheetsService == null) { + Credential credential = authorize(SheetsScopes.SPREADSHEETS); + sheetsService = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + return sheetsService; + } + + public static YouTube getYoutubeService() throws IOException { + if (youtubeService == null) { + Credential credential = authorize(YouTubeScopes.YOUTUBE); + youtubeService = new YouTube.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + return youtubeService; + } + + public static class VideoInfo { + public Date publishedAt; + public String title; + + public VideoInfo(String title, Date publishedAt) { + this.title = title; + this.publishedAt = publishedAt; + } + } + + @NoArgsConstructor + public static class DocInfo { + public String title; + public String iconLink; + public String thumbnailLink; + public String mimeType; + public String downloadUrl; + public String fileExtension; + public Integer fileSize; + } + + @NoArgsConstructor + public static class ImageInfo { + public String title; + public String downloadUrl; + public String mimeType; + public String thumbnailLink; + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/ItemsHelper.java b/src/main/java/org/ayfaar/app/utils/ItemsHelper.java index 0eb6c809..2257be45 100644 --- a/src/main/java/org/ayfaar/app/utils/ItemsHelper.java +++ b/src/main/java/org/ayfaar/app/utils/ItemsHelper.java @@ -1,6 +1,9 @@ package org.ayfaar.app.utils; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Очистка пунктов от сносок, названий Глав и Разделов * @@ -9,17 +12,18 @@ public class ItemsHelper { public static final String QUESTION = "ВОПРОС."; + public static final String punctuation = "[ \\.\\,!\\?\\):;'\"»%-]"; public static String clean(String value) { - String newContext = ""; - if(value == null) { return null; } - newContext = cleanChapter(value); - newContext = cleanSection(newContext); - return newContext; + value = cleanChapter(value); + value = cleanSection(value); + value = cleanFootnote(value); + value = cleanFootnoteStar(value); + return value; } private static String cleanChapter(String value) { @@ -113,4 +117,56 @@ public static String addQuestion(String question, String text) { return question + "\r\n" + text; } + + /** + * Удаляет из текста все сноски †, ‡ и § + */ + public static String cleanFootnote(String content) { + if(content == null) { + return null; + } + + String regexp = ".†+|.‡+|.§+"; + String separator = "†|‡|§"; + + return findFootnote(content, regexp, separator); + } + + /** + * Удаляет из текста все сноски со звёздочками но оставляет перечисления + */ + public static String cleanFootnoteStar(String content) { + if(content == null) { + return null; + } + String regexp = "([^\\d^\\n][\\*]+" + punctuation + ")|([^\\d^\\n][\\*]{2,}.)"; + String separator = "\\*"; + + return findFootnote(content, regexp, separator); + } + + private static String findFootnote(String content, String regexp, String separator) { + Pattern pattern = Pattern.compile(regexp); + Matcher matcher = pattern.matcher(content); + + while(matcher.find()) { + String phrase = matcher.group(); + content = replace(content, phrase, regexp, separator); + } + return content; + } + + private static String replace(String content, String phrase, String regexp, String separator) { + String[] letters = phrase.split(separator); + String replacing = letters[0].equals(" ") ? "" : letters[0]; + + if(letters.length > 1) { + Pattern pattern = Pattern.compile(punctuation); + Matcher matcher = pattern.matcher(letters[letters.length - 1]); + String lastLetter = matcher.find() ? "" : " "; + replacing += letters[letters.length - 1].equals(" ") ? " " : lastLetter + letters[letters.length - 1]; + } + + return content.replaceFirst(regexp, replacing); + } } diff --git a/src/main/java/org/ayfaar/app/utils/Language.java b/src/main/java/org/ayfaar/app/utils/Language.java new file mode 100644 index 00000000..1f81d42b --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/Language.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.utils; + +public enum Language { + ru, en +} diff --git a/src/main/java/org/ayfaar/app/utils/LikeExpression.java b/src/main/java/org/ayfaar/app/utils/LikeExpression.java new file mode 100644 index 00000000..e7e9610a --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/LikeExpression.java @@ -0,0 +1,84 @@ +package org.ayfaar.app.utils; + +import org.hibernate.Criteria; +import org.hibernate.HibernateException; +import org.hibernate.criterion.CriteriaQuery; +import org.hibernate.criterion.Criterion; +import org.hibernate.criterion.MatchMode; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.TypedValue; + +// http://stackoverflow.com/a/1111801/975169 +public class LikeExpression implements Criterion { + private final String propertyName; + private final String value; + private final Character escapeChar; + + protected LikeExpression( + String propertyName, + Object value) { + this(propertyName, value.toString(), (Character) null); + } + + protected LikeExpression( + String propertyName, + String value, + MatchMode matchMode) { + this( propertyName, matchMode.toMatchString( value + .toString() + .replaceAll("!", "!!") + .replaceAll("%", "!%") + .replaceAll("_", "!_")), '!' ); + } + + protected LikeExpression( + String propertyName, + String value, + Character escapeChar) { + this.propertyName = propertyName; + this.value = value; + this.escapeChar = escapeChar; + } + + public String toSqlString( + Criteria criteria, + CriteriaQuery criteriaQuery) throws HibernateException { + Dialect dialect = criteriaQuery.getFactory().getDialect(); + String[] columns = criteriaQuery.getColumnsUsingProjection( criteria, propertyName ); + if ( columns.length != 1 ) { + throw new HibernateException( "Like may only be used with single-column properties" ); + } + String lhs = lhs(dialect, columns[0]); + return lhs + " like ?" + ( escapeChar == null ? "" : " escape ?" ); + + } + + public TypedValue[] getTypedValues( + Criteria criteria, + CriteriaQuery criteriaQuery) throws HibernateException { + return new TypedValue[] { + criteriaQuery.getTypedValue( criteria, propertyName, typedValue(value) ), + criteriaQuery.getTypedValue( criteria, propertyName, escapeChar.toString() ) + }; + } + + protected String lhs(Dialect dialect, String column) { + return column; + } + + protected String typedValue(String value) { + return value; + } + + public static Criterion like(String propertyName, Object value) { + return new LikeExpression(propertyName, value); + } + + public static Criterion like(String propertyName, String value, MatchMode matchMode) { + return new LikeExpression(propertyName, value, matchMode); + } + + public static Criterion like(String propertyName, String value, Character escapeChar) { + return new LikeExpression(propertyName, value, escapeChar); + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/MorphMaster.java b/src/main/java/org/ayfaar/app/utils/MorphMaster.java new file mode 100644 index 00000000..030a8139 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/MorphMaster.java @@ -0,0 +1,26 @@ +package org.ayfaar.app.utils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class MorphMaster { + private final static String[] endings = new String[]{"ый","ость","остью","ости","о","ы","ые","ых","ым","ыми","ого","ом"}; + + public static Set getAllForms(String word) { + for (String ending : endings) { + if (word.endsWith(ending)) { + return buildForRoot(word.substring(0, word.lastIndexOf(ending))); + } + } + return new HashSet(Arrays.asList(word)); + } + + private static Set buildForRoot(String root) { + Set forms = new HashSet(); + for (String ending : endings) { + forms.add(root+ending); + } + return forms; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/ReflectionUtils.java b/src/main/java/org/ayfaar/app/utils/ReflectionUtils.java new file mode 100644 index 00000000..75b7aa3d --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/ReflectionUtils.java @@ -0,0 +1,16 @@ +package org.ayfaar.app.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class ReflectionUtils { + public static void setFinalStatic(Object target, Field field, Object newValue) throws Exception { + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(target, newValue); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/RegExpUtils.java b/src/main/java/org/ayfaar/app/utils/RegExpUtils.java index 618d0c55..605e6a3d 100644 --- a/src/main/java/org/ayfaar/app/utils/RegExpUtils.java +++ b/src/main/java/org/ayfaar/app/utils/RegExpUtils.java @@ -1,6 +1,10 @@ package org.ayfaar.app.utils; public class RegExpUtils { - public static final String w = "A-Za-zА-Яа-я0-9Ёё"; - public static final String W = "[^"+w+"]"; + public static final String w = "[A-Za-zА-Яа-я0-9Ёё]"; + public static final String W = "[^A-Za-zА-Яа-я0-9Ёё]"; + + public static String buildWordContainsRegExp(String q) { + return "(^" + q + RegExpUtils.W + "+)|(" + RegExpUtils.W + "+" + q + RegExpUtils.W + "+)|(" + RegExpUtils.W + "+" + q + "$)"; + } } diff --git a/src/main/java/org/ayfaar/app/utils/RomanNumber.java b/src/main/java/org/ayfaar/app/utils/RomanNumber.java new file mode 100644 index 00000000..627681fa --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/RomanNumber.java @@ -0,0 +1,114 @@ +package org.ayfaar.app.utils; + + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class is made for working + * with Roman numerals + * + * @author Ruav + */ +public class RomanNumber { + + /** + * Function for converting from + * Roman number to Arabic number. + * + * @param in String with roman + * numerals, which we need convert. + * @return int Converted value from + * roman numerals to arabic numerals. + */ + + public static int parse(String str) { + if (str == null) { + throw new IllegalArgumentException("Expect not null string"); + } + if (str.isEmpty()) { + throw new NullPointerException("String is empty"); + } + String in = str.toUpperCase().replaceAll(" ", ""); + + Pattern pattern = Pattern.compile("[\\sIVX]"); + Matcher matcher = pattern.matcher(in); + if (!matcher.find()) { + throw new NumberFormatException("Wrong string"); + } + String romanSymbols[] = {"I", "V", "X"}; + int[] arabicSymbols = {1, 5, 10}; + + int out = 0; + int length = in.length(); + if (length > 1) { + switch (in.toUpperCase().substring(0, 2)) { + case "IV": + out = 4 + ((length > 2) ? parse(in.substring(2)) : 0); + break; + case "IX": + out = 9 + ((length > 2) ? parse(in.substring(2)) : 0); + break; + default: + out = parse(in.substring(0, 1)) + parse(in.substring(1)); + } +// if(str.substring(0,2).equals("IV")) +// out = 4 + parseRomanNumber(str.substring(2)); +// else if(str.substring(0,2).equals("IX")) +// out = 9 + parseRomanNumber(str.substring(2)); +// else +// out = parseRomanNumber(str.substring(0,1)) + parseRomanNumber(str.substring(1)); + } else { + for (int i = 0; i < romanSymbols.length; i++) { + if (romanSymbols[i].equals(in)) { + out = arabicSymbols[i]; + break; + } + } + } + return out; + } + + /** + * Function for converting from + * Arabic number to Roman number. + * + * @param num integer with arabic + * numerals, which we need convert. + * @return String Converted value from + * arabic numerals to roman numerals. + */ + + public static String convertToRomanNumber(Integer num) { + String romanSymbols[] = {"X", "IX", "V", "IV", "I"}; + int[] arabicSymbols = {10, 9, 5, 4, 1}; + int[] numbers = {0, 0, 0, 0, 0}; + int MAXIMUM_KOEFF = 5; // берется из расчета, что для 50 идет уже символ L, а у нас он не заведен + if (num == null) { + throw new NullPointerException("Nullpointer Exception"); + } + if (num <= 0) { + throw new NumberFormatException("Less then or equals zero"); + } + if (num > arabicSymbols[0] * MAXIMUM_KOEFF) { + throw new NumberFormatException("More then maximum for this function"); + } + int temp; + String out = ""; + temp = num; + + for (int i = 0; i < arabicSymbols.length; i++) { + if (num >= arabicSymbols[i]) { + numbers[i] = temp / arabicSymbols[i]; + temp = temp % arabicSymbols[i]; + } + } + for (int i = 0; i < arabicSymbols.length; i++) { + for (int j = 0; j < numbers[i]; j++) { + out += romanSymbols[i]; + } + } + + return out; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/SearchSuggestions.java b/src/main/java/org/ayfaar/app/utils/SearchSuggestions.java new file mode 100644 index 00000000..4404348b --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/SearchSuggestions.java @@ -0,0 +1,75 @@ +package org.ayfaar.app.utils; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.utils.contents.ContentsUtils; +import org.springframework.stereotype.Service; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Arrays.asList; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.UNICODE_CASE; + +@Slf4j +@Service +public class SearchSuggestions { + + private List escapeChars = Arrays.asList("(", ")", "[", "]", "{", "}"); + + public Queue getQueue(String q) { + q = q.replace("*", ".*"); + q = q.replaceAll("\\s+", ".*"); + q = escapeRegexp(q); + q = addDuplications(q); + return new LinkedList<>(asList( + "^" + q, + "[\\s\\-]" + q, + q + )); + } + + + protected static String addDuplications(String q) { + return q.replaceAll("([A-Za-zА-Яа-яЁё])", "$1+-*$1*"); + } + + public List getSuggested(String query, List> suggestions, Collection uriWithNames, Function nameProvider) { + List list = new ArrayList<>(); + Pattern pattern = Pattern.compile(query, CASE_INSENSITIVE + UNICODE_CASE); + for (T entry : uriWithNames) { + String name = nameProvider.apply(entry); + Matcher matcher = pattern.matcher(name); + if (matcher.find() && !suggestions.contains(name) && !list.contains(name)) { + list.add(entry); + } + } + Collections.reverse(list); + return list; + } + + private String escapeRegexp(String query) { + for (String bracket : escapeChars) { + if (query.contains(bracket)) { + query = query.replace(bracket, "\\" + bracket); + } + } + return query; + } + + public Map getAllSuggestions(String q, List> suggestions){ + Map allSuggestions = new LinkedHashMap<>(); + + for (Map.Entry suggestion : suggestions) { + String key = suggestion.getKey(); + String value = suggestion.getValue(); + if(key.contains("ии:пункты:")) { + String suggestionParagraph = ContentsUtils.splitToSentence(value, q); + if(!Objects.equals(suggestionParagraph, ""))allSuggestions.put(key, suggestionParagraph); + } + else allSuggestions.put(key, value); + } + return allSuggestions; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/SoftCache.java b/src/main/java/org/ayfaar/app/utils/SoftCache.java new file mode 100644 index 00000000..bb022d49 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/SoftCache.java @@ -0,0 +1,26 @@ +package org.ayfaar.app.utils; + + +import java.lang.ref.SoftReference; +import java.util.LinkedHashMap; +import java.util.function.Supplier; + +public class SoftCache { + private LinkedHashMap> map = new LinkedHashMap<>(); + + public V getOrCreate(K key, Supplier creator) { + V value = null; + if (map.containsKey(key)) { + value = map.get(key).get(); + } + if (value == null) { + value = creator.get(); + map.put(key, new SoftReference<>(value)); + } + return value; + } + + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/StreamUtils.java b/src/main/java/org/ayfaar/app/utils/StreamUtils.java new file mode 100644 index 00000000..bf81b2d4 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/StreamUtils.java @@ -0,0 +1,31 @@ +package org.ayfaar.app.utils; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.stream.Collector; + +public class StreamUtils { + public static Collector> single() { + return Collector.of( + AtomicReference::new, new BiConsumer, E>() { + @Override + public void accept(AtomicReference ref, E e) { + if (!ref.compareAndSet(null, e)) { + throw new IllegalArgumentException("Multiple values"); + } + } + }, + (ref1, ref2) -> { + if (ref1.get() == null) { + return ref2; + } else if (ref2.get() != null) { + throw new IllegalArgumentException("Multiple values"); + } else { + return ref1; + } + }, + ref -> Optional.ofNullable(ref.get()), + Collector.Characteristics.UNORDERED); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/StringUtils.java b/src/main/java/org/ayfaar/app/utils/StringUtils.java index 0ded682c..47cfd1bf 100644 --- a/src/main/java/org/ayfaar/app/utils/StringUtils.java +++ b/src/main/java/org/ayfaar/app/utils/StringUtils.java @@ -1,7 +1,57 @@ package org.ayfaar.app.utils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; + public class StringUtils { public static String removeAllNewLines(String str) { return str != null ? str.replaceAll("\n|\r|\r\n", "") : null; } + + + public static String trim(String text) { + if (text == null || text.isEmpty()) { + return text; + } + return text.trim(); + } + // todo startsWith, array of subjects + public static String trim(String text, String subjectToTrim) { + if (text == null || text.isEmpty()) { + return text; + } + if (text.endsWith(subjectToTrim)) { + final int i = text.lastIndexOf(subjectToTrim); + return text.substring(0, i); + } + return text; + } + + + public static String markWithStrong(String text, List queries) { + List sortedQueries = new ArrayList(queries); + Collections.sort(sortedQueries, new Comparator() { + @Override + public int compare(String o1, String o2) { + return Integer.compare(o2.length(), o1.length()); + } + }); + String regexp = ""; + for (String query : sortedQueries) { + regexp += "|("+query+")"; + } + regexp = regexp.replaceFirst("\\|", ""); + + text = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) + .matcher(text) + .replaceAll("$0"); + return text; + } + + public static String firstUpper(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } } diff --git a/src/main/java/org/ayfaar/app/utils/SysLogListener.java b/src/main/java/org/ayfaar/app/utils/SysLogListener.java new file mode 100644 index 00000000..85ef3ea8 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/SysLogListener.java @@ -0,0 +1,35 @@ +package org.ayfaar.app.utils; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.event.SysLogEvent; +import org.ayfaar.app.model.ActionEvent; +import org.ayfaar.app.services.moderation.Action; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; + +@Component +@Slf4j +public class SysLogListener { + private final CommonDao commonDao; +// final SysLogService sysLogService; + + @Inject + public SysLogListener(CommonDao commonDao) { + this.commonDao = commonDao; + } + + @Async + @EventListener + private void listenForEvents(SysLogEvent event) { + String message = String.format("Системное событие от %s: %s", event.getSource(), event.getMessage()); + final ActionEvent actionEvent = new ActionEvent(); + actionEvent.setAction(Action.SYS_EVENT); + actionEvent.setMessage(message); + actionEvent.setCreatedBy(-1); + commonDao.save(actionEvent); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/TermService.java b/src/main/java/org/ayfaar/app/utils/TermService.java new file mode 100644 index 00000000..1b4449f2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/TermService.java @@ -0,0 +1,53 @@ +package org.ayfaar.app.utils; + +import org.ayfaar.app.dao.TermDao; +import org.ayfaar.app.model.LinkType; +import org.ayfaar.app.model.Term; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Интерфейс взаимодействия с предварительно загруженными всеми терминами + */ +public interface TermService { + /** + * Возвращает термины во всех падежах и соответствующие объекты, содержащие однозначные имена + * (обычно в именительном падеже). Например "времени" => "Время", "времён" => "Время" и т. д. + */ + List> getAll(); + List getAllInfoTerms(); + Optional get(String name); + Optional getByUri(String uri); + Optional getMainOrThis(String name); + Term getTerm(String name); + void reload(); + + void save(Term term); + + interface TermProvider { + String getName(); + String getUri(); + boolean hasShortDescription(); + Optional getMain(); + default TermProvider getMainOrThis() { + return getMain().orElse(this); + } + Term getTerm(); + List getMorphs(); + List getAliases(); + List getAbbreviations(); + Optional getCode(); + LinkType getType(); + boolean hasMain(); + boolean isAbbreviation(); + boolean isAlias(); + boolean isCode(); + boolean hasCode(); + List getAllAliasesWithAllMorphs(); + List getAllAliasesAndAbbreviationsWithAllMorphs(); + + Optional getShortDescription(); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/TermServiceImpl.java b/src/main/java/org/ayfaar/app/utils/TermServiceImpl.java new file mode 100644 index 00000000..d73e1beb --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/TermServiceImpl.java @@ -0,0 +1,321 @@ +package org.ayfaar.app.utils; + + +import lombok.Data; +import lombok.Getter; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.dao.LinkDao; +import org.ayfaar.app.dao.TermDao; +import org.ayfaar.app.event.NewLinkEvent; +import org.ayfaar.app.model.*; +import org.ayfaar.app.services.EntityLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.sort; +import static java.util.regex.Pattern.compile; +import static org.ayfaar.app.model.LinkType.*; +import static org.ayfaar.app.utils.UriGenerator.getValueFromUri; +import static org.springframework.util.StringUtils.isEmpty; + +@Component +public class TermServiceImpl implements TermService { + private static final Logger logger = LoggerFactory.getLogger(TermService.class); + + private final CommonDao commonDao; + private EntityLoader entityLoader; + private final TermDao termDao; + private final LinkDao linkDao; + + private Map links; + private Map aliasesMap; + // names - provider map + private ArrayList> sortedList; + private List termsInfo; + + @Autowired + public TermServiceImpl(LinkDao linkDao, TermDao termDao, CommonDao commonDao, EntityLoader entityLoader) { + this.linkDao = linkDao; + this.termDao = termDao; + this.commonDao = commonDao; + this.entityLoader = entityLoader; + } + + @PostConstruct + public void load() { + logger.info("Terms loading..."); + aliasesMap = new HashMap<>(); + List allTermMorphs = commonDao.getAll(TermMorph.class); + termsInfo = termDao.getAllTermInfo(); + List allSynonyms = linkDao.getAllSynonyms(); + + links = new HashMap<>(); + for(Link link : allSynonyms) { + links.put(link.getUid2().getUri(), new LinkInfo(link.getType(), (Term)link.getUid1())); + } + + for(TermDao.TermInfo info : termsInfo) { + String uri = UriGenerator.generate(Term.class, info.getName()); + String mainTermUri = null; + + if(links.containsKey(uri)) { + mainTermUri = links.get(uri).getMainTerm().getUri(); + } + aliasesMap.put(info.getName().toLowerCase(), new TermProviderImpl(uri, mainTermUri, info.isHasShortDescription())); + } + + for(TermMorph morph : allTermMorphs) { + final TermProvider termProvider = aliasesMap.get(getValueFromUri(Term.class, morph.getTermUri()).toLowerCase()); + aliasesMap.put(morph.getName().toLowerCase(), termProvider); + } + + // prepare sorted List by term name length, longest terms first + sortedList = new ArrayList>(aliasesMap.entrySet()); + sort(sortedList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return Integer.compare(o2.getKey().length(), o1.getKey().length()); + } + }); + logger.info("Terms loading finish"); + } + + public void reload() { + load(); + } + + @Override + public void save(Term term) { + commonDao.save(term); + load(); + } + + @Data + private class LinkInfo { + private LinkType type; + private Term mainTerm; + + private LinkInfo(LinkType type, Term term) { + this.type = type; + this.mainTerm = term; + } + } + + public class TermProviderImpl implements TermProvider { + @Getter + private String uri; + private String mainTermUri; + private boolean hasShortDescription; + + public TermProviderImpl(String uri, String mainTermUri, boolean hasShortDescription) { + this.uri = uri; + this.mainTermUri = mainTermUri; + this.hasShortDescription = hasShortDescription; + } + + public String getName() { + return getValueFromUri(Term.class, uri); + } + + public boolean hasShortDescription() { + return hasShortDescription; + } + + public Term getTerm() { +// return termDao.get(uri); + return entityLoader.get(uri); + } + + public List getAliases() { + return getListProviders(ALIAS, getName()); + } + + public List getAbbreviations() { + return getListProviders(ABBREVIATION, getName()); + } + + public Optional getCode() { + List codes = getListProviders(CODE, getName()); + return codes.size() > 0 ? Optional.of(codes.get(0)) : Optional.empty(); + } + + public Optional getMain() { + return Optional.ofNullable(hasMain() ? aliasesMap.get(getValueFromUri(Term.class, mainTermUri).toLowerCase()) : null); + } + + public List getMorphs() { + List morphs = new ArrayList(); + for (Map.Entry map : aliasesMap.entrySet()) { + if(map.getValue().getUri().equals(getUri())) { + morphs.add(map.getKey()); + } + } + return morphs; + } + + public LinkType getType() { + return links.get(uri) != null ? links.get(uri).getType() : null; + } + + public boolean hasMain() { + return mainTermUri != null; + } + + public boolean isAbbreviation() { + return ABBREVIATION.equals(getType()); + } + + public boolean isAlias() { + return ALIAS.equals(getType()); + } + + public boolean isCode() { + return CODE.equals(getType()); + } + + @Override + public boolean hasCode() { + return !getListProviders(CODE, getName()).isEmpty(); + } + + @Override + public List getAllAliasesWithAllMorphs() { + List list = getMorphs(); + List aliasesSearchQueries = getAllMorphs(getAllAliases()); + list.addAll(aliasesSearchQueries); + return list; + } + + public List getAllAliasesAndAbbreviationsWithAllMorphs() { + final List list = getAllAliasesWithAllMorphs(); + list.addAll(getAllMorphs(getAbbreviations())); + return list; + } + + @Override + public Optional getShortDescription() { + return hasShortDescription ? Optional.of(getTerm().getShortDescription()) : Optional.empty(); + } + + List getAllMorphs(List providers) { + List morphs = new ArrayList(); + + for(TermProvider provider : providers) { + morphs.addAll(provider.getMorphs()); + } + return morphs; + } + + List getAllAliases() { + List aliases = new ArrayList(); + TermProvider code = getCode().isPresent() ? getCode().get() : null; + + aliases.addAll(getAliases()); + aliases.addAll(getAbbreviations()); + if(code != null) { + aliases.add(getCode().get()); + } + return aliases; + } + } + + + @Override + public Optional get(String name) { + return Optional.ofNullable(aliasesMap.get(name.toLowerCase())); + } + + @Override + public Optional getByUri(String uri) { + return Optional.ofNullable(aliasesMap.get(getValueFromUri(Term.class, uri))); + } + + @Override + public Optional getMainOrThis(String name) { + final Optional providerOpt = get(name); + if (providerOpt.isPresent() && providerOpt.get().getMain().isPresent()) { + return providerOpt.get().getMain(); + } + return providerOpt; + } + + @Override + public List> getAll() { + return sortedList; + } + + @Override + public List getAllInfoTerms() { + return termsInfo; + } + + @Override + public Term getTerm(String name) { + TermProvider termProvider = aliasesMap.get(name.toLowerCase()); + return termProvider != null ? termProvider.getTerm() : null; + } + + private List getListProviders(LinkType type, String name) { + List providers = new ArrayList(); + + for(Map.Entry link : links.entrySet()) { + if(link.getValue().getType() == type && link.getValue().getMainTerm().getName().equals(name)) { + providers.add(aliasesMap.get(getValueFromUri(Term.class, link.getKey().toLowerCase()))); + } + } + return providers; + } + + @Deprecated // for old version and mediawiki sync support + public List findTermsInside(String content) { + Set contains = new HashSet(); + content = content.toLowerCase(); + + for (Map.Entry entry : sortedList) { + String key = entry.getKey(); + Matcher matcher = compile("((" + RegExpUtils.W + ")|^)" + key + + "((" + RegExpUtils.W + ")|$)", Pattern.UNICODE_CHARACTER_CLASS) + .matcher(content); + if (matcher.find()) { + contains.add(entry.getValue().getTerm()); + content = content.replaceAll(key, ""); + } + } + + List sorted = new ArrayList(contains); + sorted.sort(Comparator.comparing(o -> o.getName().toLowerCase())); + return sorted; + } + + @EventListener + private void onNewLink(NewLinkEvent event) { + final Link link = event.link; + if (link.getUid1() instanceof Term && link.getUid2() instanceof Term) { + onTermsLinked((Term) link.getUid1(), (Term) link.getUid2(), link.getType()); + } + } + + private void onTermsLinked(Term mainTerm, Term aliasTerm, LinkType linkType) { + if (linkType == LinkType.ALIAS) { + boolean mainTermChanged = false; + if (isEmpty(mainTerm.getShortDescription())) { + mainTerm.setShortDescription(aliasTerm.getShortDescription()); + mainTermChanged = true; + } + if (isEmpty(mainTerm.getDescription())) { + mainTerm.setDescription(aliasTerm.getDescription()); + mainTermChanged = true; + } + if (mainTermChanged) termDao.save(mainTerm); + } + reload(); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/TermsFinder.java b/src/main/java/org/ayfaar/app/utils/TermsFinder.java new file mode 100644 index 00000000..9b98e609 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/TermsFinder.java @@ -0,0 +1,101 @@ +package org.ayfaar.app.utils; + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.dao.CommonDao; +import org.ayfaar.app.model.TermParagraph; +import org.springframework.stereotype.Service; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.*; + +@Slf4j +@Service +public class TermsFinder { + + @Inject TermService termService; + @Inject ContentsService contentsService; + @Inject + CommonDao commonDao; + + public Map getTermsWithFrequency(String content){ + return getTermsWithFrequency(content, null); + } + + public Map getTermsWithFrequency(String content, String term){ + + if (content == null || content.isEmpty()) log.info("Content is Empty!"); + + Map termFrequency = new HashMap<>(); + content = content.replace("–","-").replace("—","-"); + StringBuilder result = new StringBuilder(content); + + List> allTerms = (term != null) ? getSimpleTermProvider(term) : termService.getAll(); + + + for (Map.Entry entry : allTerms) { + + int frequency = 0; + String mainTerm = null; + // получаем слово связаное с термином, напрмер "времени" будет связано с термином "Время" + String word = entry.getKey(); + // составляем условие по которому проверяем есть ли это слов в тексте + Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|])|^)(около|слабо|высоко|не|анти|разно|дву|трёх|четырёх|пяти|шести|семи|восьми|девяти|десяти|внутри|пост|меж|мощно|взаимо|внутри|не)?(" + + word + ")(([^A-Za-zА-Яа-я0-9Ёё\\]\\|])|$)", UNICODE_CHARACTER_CLASS | UNICODE_CASE | CASE_INSENSITIVE); + Matcher contentMatcher = pattern.matcher(content); + // если есть: + + if (contentMatcher.find()) { + int offset = 0; + Matcher matcher = pattern.matcher(result); + + while (offset < result.length() && matcher.find(offset)) { + + final TermService.TermProvider termProvider = entry.getValue(); + mainTerm = termProvider.getMainOrThis().getName(); + + content = contentMatcher.replaceAll(" ");// убираем обработанный термин, чтобы не заменить его более мелким + + offset = matcher.end(); + frequency++; + } + + if(mainTerm != null) + termFrequency.put(mainTerm, termFrequency.containsKey(mainTerm) ? termFrequency.get(mainTerm) + frequency : frequency); + } + } + return termFrequency; + } + + public void updateTermParagraphForTerm(String new_term){ + + contentsService.getAllParagraphs().forEach(paragraph -> + { + Map termsWithFrequency = getTermsWithFrequency(paragraph.description(), new_term); + termsWithFrequency.keySet().parallelStream().map(term -> + new TermParagraph(paragraph.code(), term)).forEach(t -> + commonDao.save(TermParagraph.class,t)); + }); + } + + private List> getSimpleTermProvider(String term) { + Map termProviderMap = new HashMap<>(); + TermService.TermProvider termProvider = termService.get(term).get(); + List morphs = termProvider.getMorphs(); + for (String morph : + morphs) { + + if(!termProviderMap.containsKey(morph)) { + termProviderMap.put(morph, termProvider); + } + + } + + return new ArrayList<>(termProviderMap.entrySet()); + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/TermsMarker.java b/src/main/java/org/ayfaar/app/utils/TermsMarker.java new file mode 100644 index 00000000..d711cc77 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/TermsMarker.java @@ -0,0 +1,106 @@ +package org.ayfaar.app.utils; + +import org.ayfaar.app.utils.TermService.TermProvider; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static java.util.regex.Pattern.*; + +@Component +public class TermsMarker { + public final static String TAG_NAME = "term"; + + @Inject TermService termService; + + /** + * Пометить все термины в тексте тегами . + * Например: текст до термином текст после + * + * За основу взять org.ayfaar.app.synchronization.mediawiki.TermSync#markTerms + * + * @param content исходный текст с терминами + * @return текст с тегами терминов + */ + public String mark(String content) { + if (content == null || content.isEmpty()) return content; + + content = content.replace("–","-").replace("—","-"); + // копируем исходный текст, в этой копии мы будем производить тегирование слов + StringBuilder result = new StringBuilder(content); + //перед обходом отсортируем по длине термина, сначала самые длинные + + + for (Map.Entry entry : termService.getAll()) { + // получаем слово связаное с термином, напрмер "времени" будет связано с термином "Время" + String word = entry.getKey(); + // составляем условие по которому проверяем есть ли это слов в тексте + //Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|\\-])|^)(" + word + Pattern pattern = compile("(([^A-Za-zА-Яа-я0-9Ёё\\[\\|])|^)(около|слабо|высоко|не|анти|разно|дву|трёх|четырёх|пяти|шести|семи|восьми|девяти|десяти|внутри|пост|меж|мощно|взаимо|внутри|не)?(" + + word + ")(([^A-Za-zА-Яа-я0-9Ёё\\]\\|])|$)", UNICODE_CHARACTER_CLASS | UNICODE_CASE | CASE_INSENSITIVE); + Matcher contentMatcher = pattern.matcher(content); + // если есть: + if (contentMatcher.find()) { + // ищем в результирующем тексте + Matcher matcher = pattern.matcher(result); + int offset = 0; + // if (matcher.find()) { + //перенесем обрамления для каждого слова - одно слово может встречаться несколько раз с разными обрамл. + while (offset < result.length() && matcher.find(offset)) { + offset = matcher.end(); + //убедимся что это не уже обработанный термин, а следующий(в им.падеже) + //String sub = result.substring(matcher.start() - 3, matcher.start()); + //if (sub.equals("id=")) { + if (wordInTag(result.substring(0, matcher.start()))) { + continue; + } + // сохраняем найденое слово из текста так как оно может быть в разных регистрах, + // например с большой буквы, или полностью большими буквами + String wordPrefix = matcher.group(3); + String foundWord = matcher.group(4); + String charBefore = matcher.group(2) != null ? matcher.group(2) : ""; + String charAfter = matcher.group(5) != null ? matcher.group(5) : ""; + // формируем маску для тегирования, title="%s" это дополнительное требования, не описывал ещё в задаче + //String replacer = format("%s%s%s", + //пока забыли о title="...." + final TermProvider termProvider = entry.getValue(); + boolean hasMainTerm = termProvider.hasMain(); + final TermProvider mainTermProvider = hasMainTerm ? termProvider.getMain().get() : null; + boolean hasShortDescription = hasMainTerm ? mainTermProvider.hasShortDescription() : termProvider.hasShortDescription(); + + String attributes = hasShortDescription ? " has-short-description=\"true\"" : ""; + attributes += hasMainTerm ? format(" title=\"%s\"", mainTermProvider.getName()) : ""; + + String replacer = format("%s%s%s%s", + charBefore, + wordPrefix == null ? "" : wordPrefix, + hasMainTerm ? mainTermProvider.getName() : termProvider.getName(), + attributes, + foundWord, + charAfter + ); + //System.out.println("charbefore " + charBefore + " entry " + entry.getValue().getTerm().getName()); + // заменяем найденое слово тегированным вариантом + //result = matcher.replaceAll(replacer); + result.replace(matcher.start(), matcher.end(), replacer); + //увеличим смещение с учетом замены + offset = matcher.start() + replacer.length(); + // убираем обработанный термин, чтобы не заменить его более мелким + content = contentMatcher.replaceAll(" "); + } + } + } + return result.toString(); + } + + private boolean wordInTag(String substring) { + int startTag = substring.lastIndexOf(""); + + return startTag >= 0 && startTag > endTag; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/TermsTaggingUpdater.java b/src/main/java/org/ayfaar/app/utils/TermsTaggingUpdater.java new file mode 100644 index 00000000..2fc82d7f --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/TermsTaggingUpdater.java @@ -0,0 +1,94 @@ +package org.ayfaar.app.utils; + +import org.apache.commons.lang.time.DurationFormatUtils; +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.dao.LinkDao; +import org.ayfaar.app.dao.TermDao; +import org.ayfaar.app.dao.TermMorphDao; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.model.Link; +import org.ayfaar.app.model.Term; +import org.hibernate.criterion.MatchMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class TermsTaggingUpdater { + @Autowired + private TermsMarker termsMarker; + @Autowired + private TermMorphDao termMorphDao; + @Autowired + private ItemDao itemDao; + @Autowired + private LinkDao linkDao; + @Autowired + private TermDao termDao; +// @Autowired ApplicationEventPublisher publisher; + + public void update(String termName) { + long time = System.currentTimeMillis(); + final List aliases = termMorphDao.getAllMorphs(termName); + updateTerms(termDao.getLike("description", aliases, MatchMode.ANYWHERE)); + updateTerms(termDao.getLike("shortDescription", aliases, MatchMode.ANYWHERE)); + final List items = itemDao.getLike("content", aliases, MatchMode.ANYWHERE); + updateItems(items); + updateLinks(linkDao.getLike("quote", aliases, MatchMode.ANYWHERE)); + + final String duration = DurationFormatUtils.formatDuration(System.currentTimeMillis() - time, "HH:mm:ss"); +// publisher.publishEvent(new SimplePushEvent(format("Тегирование для %s завершено за %s", termName, duration))); + } + + public void updateAllContent() { + updateItems(itemDao.getAll()); + } + + + public void updateAllTerms() { + updateTerms(termDao.getAll()); + } + + public void updateAllQuotes() { + updateLinks(linkDao.getAll()); + } + + public void updateItems(List items) { + for (Item item : items) { + update(item); + } + } + + private void updateTerms(List terms) { + for (Term term : terms) { + term.setTaggedDescription(termsMarker.mark(term.getDescription())); + term.setTaggedShortDescription(termsMarker.mark(term.getShortDescription())); + termDao.save(term); + } + } + + private void updateLinks(List links) { + for (Link link : links) { +// if (link.getQuote() != null && link.getTaggedQuote() == null) { + link.setTaggedQuote(termsMarker.mark(link.getQuote())); + linkDao.save(link); +// System.out.println(link.getLinkId()); +// } + } + } + + public void updateSingle(String morphAlias) { + long time = System.currentTimeMillis(); + updateItems(itemDao.getLike("content", morphAlias, MatchMode.ANYWHERE)); + updateLinks(linkDao.getLike("quote", morphAlias, MatchMode.ANYWHERE)); + + final String duration = DurationFormatUtils.formatDuration(System.currentTimeMillis() - time, "HH:mm:ss"); +// publisher.publishEvent(new SimplePushEvent(format("Тегирование для %s завершено за %s", morphAlias, duration))); + } + + public void update(Item item) { + item.setTaggedContent(termsMarker.mark(item.getContent())); + itemDao.save(item); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/Transaction.java b/src/main/java/org/ayfaar/app/utils/Transaction.java new file mode 100644 index 00000000..7f1b42c2 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/Transaction.java @@ -0,0 +1,36 @@ +package org.ayfaar.app.utils; + +import java.util.LinkedList; +import java.util.List; + +public class Transaction implements Runnable { + private List register = new LinkedList<>(); + + public Transaction register(Runnable run, Runnable rollback) { + register.add(new Operation(run, rollback)); + return this; + } + + public void run() { + List rollbackChain = new LinkedList<>(); + register.stream().forEachOrdered(operation -> { + try { + operation.run.run(); + rollbackChain.add(0, operation.rollback); + } catch (Exception e) { + rollbackChain.forEach(Runnable::run); + throw new RuntimeException("Transaction aborted and reverted", e); + } + }); + } + + private class Operation { + private final Runnable run; + private final Runnable rollback; + + private Operation(Runnable run, Runnable rollback) { + this.run = run; + this.rollback = rollback; + } + } +} diff --git a/src/main/java/org/ayfaar/app/utils/Transformer.java b/src/main/java/org/ayfaar/app/utils/Transformer.java new file mode 100644 index 00000000..d25c0dc1 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/Transformer.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.utils; + +public interface Transformer { + public T transform(E value); +} diff --git a/src/main/java/org/ayfaar/app/utils/Transliterator.java b/src/main/java/org/ayfaar/app/utils/Transliterator.java new file mode 100644 index 00000000..a3553f63 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/Transliterator.java @@ -0,0 +1,93 @@ +package org.ayfaar.app.utils; + +import java.util.HashMap; +import java.util.Map; + +public class Transliterator { + + private static final Map charMap = new HashMap<>(); + + static { + charMap.put('А', "A"); + charMap.put('Б', "B"); + charMap.put('В', "V"); + charMap.put('Г', "G"); + charMap.put('Д', "D"); + charMap.put('Е', "E"); + charMap.put('Ё', "E"); + charMap.put('Ж', "Zh"); + charMap.put('З', "Z"); + charMap.put('И', "I"); + charMap.put('Й', "Y"); + charMap.put('К', "K"); + charMap.put('Л', "L"); + charMap.put('М', "M"); + charMap.put('Н', "N"); + charMap.put('О', "O"); + charMap.put('П', "P"); + charMap.put('Р', "R"); + charMap.put('С', "S"); + charMap.put('Т', "T"); + charMap.put('У', "U"); + charMap.put('Ф', "F"); + charMap.put('Х', "H"); + charMap.put('Ц', "C"); + charMap.put('Ч', "Ch"); + charMap.put('Ш', "Sh"); + charMap.put('Щ', "Sh"); + charMap.put('Ъ', "'"); + charMap.put('Ы', "Y"); + charMap.put('Ь', "'"); + charMap.put('Э', "E"); + charMap.put('Ю', "U"); + charMap.put('Я', "Ya"); + charMap.put('а', "a"); + charMap.put('б', "b"); + charMap.put('в', "v"); + charMap.put('г', "g"); + charMap.put('д', "d"); + charMap.put('е', "e"); + charMap.put('ё', "e"); + charMap.put('ж', "zh"); + charMap.put('з', "z"); + charMap.put('и', "i"); + charMap.put('й', "y"); + charMap.put('к', "k"); + charMap.put('л', "l"); + charMap.put('м', "m"); + charMap.put('н', "n"); + charMap.put('о', "o"); + charMap.put('п', "p"); + charMap.put('р', "r"); + charMap.put('с', "s"); + charMap.put('т', "t"); + charMap.put('у', "u"); + charMap.put('ф', "f"); + charMap.put('х', "h"); + charMap.put('ц', "c"); + charMap.put('ч', "ch"); + charMap.put('ш', "sh"); + charMap.put('щ', "sh"); + charMap.put('ъ', "'"); + charMap.put('ы', "y"); + charMap.put('ь', "'"); + charMap.put('э', "e"); + charMap.put('ю', "u"); + charMap.put('я', "ya"); + + } + + public static String transliterate(String string) { + StringBuilder transliteratedString = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + Character ch = string.charAt(i); + String charFromMap = charMap.get(ch); + if (charFromMap == null) { + transliteratedString.append(ch); + } else { + transliteratedString.append(charFromMap); + } + } + return transliteratedString.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/UpdateService.java b/src/main/java/org/ayfaar/app/utils/UpdateService.java new file mode 100644 index 00000000..02bc2867 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/UpdateService.java @@ -0,0 +1,27 @@ +package org.ayfaar.app.utils; + + +import lombok.extern.slf4j.Slf4j; +import org.ayfaar.app.event.TermAddEvent; +import org.ayfaar.app.services.itemRange.ItemRangeService; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import javax.inject.Inject; + + +@Service +@Slf4j +public class UpdateService { + @Inject TermService termService; + @Inject TermsFinder termsFinder; + @Inject ItemRangeService itemRangeService; + + @EventListener + @Async + public void updateTermServices(TermAddEvent termAddEvent){ + termService.reload(); + termsFinder.updateTermParagraphForTerm(termAddEvent.getTerm()); + itemRangeService.reload(); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/UriGenerator.java b/src/main/java/org/ayfaar/app/utils/UriGenerator.java index a12d4703..d926f5ba 100644 --- a/src/main/java/org/ayfaar/app/utils/UriGenerator.java +++ b/src/main/java/org/ayfaar/app/utils/UriGenerator.java @@ -1,15 +1,21 @@ package org.ayfaar.app.utils; +import lombok.extern.log4j.Log4j; import org.apache.commons.beanutils.PropertyUtils; import org.ayfaar.app.annotations.Uri; -import org.ayfaar.app.model.UID; +import org.ayfaar.app.model.*; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.id.IdentifierGenerator; +import org.hibernate.internal.SessionImpl; +import org.springframework.util.Assert; import java.io.Serializable; +import java.util.stream.Stream; -//@Component +import static org.ayfaar.app.utils.StreamUtils.single; + +@Log4j public class UriGenerator implements IdentifierGenerator { public static String generate(UID object) throws HibernateException { return (String) new UriGenerator().generate(null, object); @@ -19,6 +25,19 @@ public static String generate(UID object) throws HibernateException { public Serializable generate(SessionImplementor session, Object object) throws HibernateException { Uri annotation = object.getClass().getAnnotation(Uri.class); String nameSpace = annotation.nameSpace(); + if (object instanceof ItemsRange) { + return nameSpace + ((ItemsRange) object).getCode(); + } + if (object instanceof HasSequence) { + Sequence sequence = null; + try { + sequence = (Sequence) ((HasSequence) object).getSequence().newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + log.error(e); + } + ((SessionImpl) session).getSessionFactory().getCurrentSession().save(sequence); + return nameSpace + sequence.getSeq(); + } String uri = null; try { Object property = PropertyUtils.getProperty(object, annotation.field()); @@ -39,4 +58,14 @@ public static String getValueFromUri(Class objectClass, String uri) { Uri annotation = (Uri) objectClass.getAnnotation(Uri.class); return uri.replace(annotation.nameSpace(), ""); } + + @SuppressWarnings("unchecked") + public static Class getClassByUri(String uri) { + return Stream.of(Item.class, Article.class, Category.class, ItemsRange.class, Resource.class, VideoResource.class, Term.class, Topic.class, Document.class, Record.class, Image.class) + .filter(clazz -> { + Uri annotation = clazz.getAnnotation(Uri.class); + Assert.notNull(annotation, "Uri annotation not found for class "+clazz); + return uri.startsWith(annotation.nameSpace()); + }).collect(single()).orElseThrow(() -> new RuntimeException("Has no class for uri "+uri)); + } } \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/contents/CategoryPresentation.java b/src/main/java/org/ayfaar/app/utils/contents/CategoryPresentation.java new file mode 100644 index 00000000..52dbb868 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/contents/CategoryPresentation.java @@ -0,0 +1,62 @@ +package org.ayfaar.app.utils.contents; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +@Data +@NoArgsConstructor +public class CategoryPresentation { + private String name; + private String uri; + private String description; + private String content; + private String previous; + private String next; + private String from; + private String to; + private List parents; + private List children; + + public CategoryPresentation(String name, String uri, String description, List children){ + this.name = name; + this.uri = uri; + this.description = description; + this.children = children; + } + + public CategoryPresentation(String name, String uri, String description, Optional previous, Optional next, + List parents, List children){ + + this(name, uri, description, children); + this.previous = previous.isPresent() ? previous.get() : null; + this.next= next.isPresent() ? next.get() : null; + this.parents = parents; + } + + public CategoryPresentation(String name, String uri, String description, Optional previous, Optional next, + List parents, List children, String from, String to){ + + this(name, uri, description, previous, next, parents, children); + this.from = from; + this.to = to; + } + + public CategoryPresentation(String name, String uri, String description) { + this(name, uri, description, null); + } + + public CategoryPresentation(String name, String uri) { + this(name, uri, null); + } + + public CategoryPresentation(String paragraphCode, String uri, String description, String from, String to) { + this(paragraphCode, uri, description, null); + this.from = from; + this.to = to; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/contents/ContentsHelper.java b/src/main/java/org/ayfaar/app/utils/contents/ContentsHelper.java new file mode 100644 index 00000000..eee397fc --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/contents/ContentsHelper.java @@ -0,0 +1,124 @@ +package org.ayfaar.app.utils.contents; + +import org.ayfaar.app.controllers.ItemController; +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.model.Item; +import org.ayfaar.app.utils.ContentsService; +import org.ayfaar.app.utils.ContentsService.CategoryProvider; +import org.ayfaar.app.utils.ContentsService.ParagraphProvider; +import org.ayfaar.app.utils.TermsMarker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static java.lang.String.format; +import static org.ayfaar.app.utils.StringUtils.trim; + +@Component +public class ContentsHelper { + @Autowired ItemDao itemDao; + @Autowired TermsMarker marker; + @Autowired + ContentsService contentsService; + + private final int SUBCATEGORY_COUNT = 2; + + public CategoryPresentation createContents(String categoryName) { + Optional providerOpt = contentsService.get(categoryName); + if (!providerOpt.isPresent()) { + throw new RuntimeException(format("Category `%s` not found", categoryName)); + } + + if(providerOpt.get() instanceof ParagraphProvider) { + ParagraphProvider p = (ParagraphProvider) providerOpt.get(); + return new CategoryPresentation(p.code(), p.uri(), + marker.mark(trim(p.name())), p.previousUri(), p.nextUri(), + createParentPresentation(p.parents()), + getParagraphSubCategory(getItems(p), 1), + p.from(), p.to()); + } else { + CategoryProvider c = (CategoryProvider) providerOpt.get(); + return new CategoryPresentation(extractCategoryName(c.code()), c.uri(), + marker.mark(trim(c.description())), c.previousUri(), c.nextUri(), + createParentPresentation(c.parents()), + createChildrenPresentation(c.children(), 0)); + } + } + + private List createChildrenPresentation(List categories, int count) { + if(count >= SUBCATEGORY_COUNT) return null; + + List childrenPresentations = new ArrayList(); + + count++; + + for (ContentsService.ContentsProvider category : categories) { + if (category instanceof CategoryProvider) { + childrenPresentations.add(new CategoryPresentation( + extractCategoryName(category.code()), category.uri(), category.description(), + createChildrenPresentation(((CategoryProvider) category).children(), count))); + + } else if (count < 2) { + ParagraphProvider paragraph = (ParagraphProvider) category; + childrenPresentations.add(new CategoryPresentation(category.code(), + category.uri(), trim(category.name()), paragraph.from(), paragraph.to())); + } + } + + return childrenPresentations; + } + + private List getParagraphSubCategory(List items, int count) { + if(count >= SUBCATEGORY_COUNT) return null; + List listPresentations = new ArrayList(); + for (Item item : items) { + CategoryPresentation presentation = new CategoryPresentation(item.getNumber(), item.getUri()); + presentation.setContent(item.getTaggedContent()); + + listPresentations.add(presentation); + } + return listPresentations; + } + + List getItems(ParagraphProvider p) { + List items = new ArrayList<>(); + Item currentItem = itemDao.getByNumber(p.from()); + if (currentItem == null) throw new RuntimeException("No item found for paragraph " + p.code()); + + String itemNumber = currentItem.getNumber(); + String endNumber = p.to(); + + items.add(currentItem); + while(!itemNumber.equals(endNumber)) { + itemNumber = ItemController.getNext(itemNumber); + currentItem = itemDao.getByNumber(itemNumber); + items.add(currentItem); + } +// if (itemNumber.startsWith("1.") || itemNumber.startsWith("2.") || itemNumber.startsWith("3.")) items.remove(items.size()-1); + return items; + } + + String extractCategoryName(String categoryName) { + String[] names = categoryName.split("/"); + return names[names.length-1].trim(); + } + + /** + * For parents presentations we need only name, uri and description. children and parents should be null + * + * @param categories + * @return + */ + List createParentPresentation(List categories) { + List presentations = new ArrayList(); + + for(CategoryProvider category : categories) { + presentations.add(new CategoryPresentation(extractCategoryName(category.code()), + category.uri(), trim(category.description()))); + } + return presentations; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/contents/ContentsUtils.java b/src/main/java/org/ayfaar/app/utils/contents/ContentsUtils.java new file mode 100644 index 00000000..66af2f58 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/contents/ContentsUtils.java @@ -0,0 +1,30 @@ +package org.ayfaar.app.utils.contents; + +import org.springframework.stereotype.Component; + +@Component +public class ContentsUtils { + + public static final String SENTENCE_ENDS = ".!?"; + + public static String splitToSentence(String paragraph, String search){ + + String p1 = paragraph; + String findString = search.toLowerCase(); + String result = ""; + + StringBuilder buf = new StringBuilder(); + + for ( char c : p1.toCharArray() ) { + if ( c == '\n' ) + c = ' '; + buf.append(c); + if ( SENTENCE_ENDS.indexOf(c) > -1 ) { + if(buf.toString().toLowerCase().contains(findString)) result = buf.toString().trim(); + buf = new StringBuilder(); + } + } + return result; + } +} + diff --git a/src/main/java/org/ayfaar/app/utils/exceptions/ConfirmationRequiredException.java b/src/main/java/org/ayfaar/app/utils/exceptions/ConfirmationRequiredException.java new file mode 100644 index 00000000..a8a7842c --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/exceptions/ConfirmationRequiredException.java @@ -0,0 +1,12 @@ +package org.ayfaar.app.utils.exceptions; + +import org.ayfaar.app.model.PendingAction; + +public class ConfirmationRequiredException extends LogicalException { + public PendingAction action; + + public ConfirmationRequiredException(PendingAction action) { + super(ExceptionCode.CONFIRMATION_REQUIRED); + this.action = action; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/exceptions/ExceptionCode.java b/src/main/java/org/ayfaar/app/utils/exceptions/ExceptionCode.java new file mode 100644 index 00000000..1a6deca6 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/exceptions/ExceptionCode.java @@ -0,0 +1,25 @@ +package org.ayfaar.app.utils.exceptions; + +public enum ExceptionCode { + + TOPIC_NOT_FOUND("Topic for {} not found"), + ITEM_NOT_FOUND("Item {} not found"), + CONFIRMATION_REQUIRED("Action {} require confirmation"), + LINK_NOT_FOUND("Link for {} and {} not found"), + USER_NOT_FOUND("User with email {} not found"), + ACCESS_DENIED("Access denied"), + ROLE_NOT_FOUND("Role for {} not found"); + + + private String message; + + ExceptionCode(String message) { + this.message = message; + } + + public String getMessage() { return message; } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/exceptions/Exceptions.java b/src/main/java/org/ayfaar/app/utils/exceptions/Exceptions.java new file mode 100644 index 00000000..d62fed9d --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/exceptions/Exceptions.java @@ -0,0 +1,24 @@ +package org.ayfaar.app.utils.exceptions; + +public enum Exceptions { + + TOPIC_NOT_FOUND("Topic for {} not found"), + ITEM_NOT_FOUND("Item {} not found"), + CONFIRMATION_REQUIRED("Action {} require confirmation"), + LINK_NOT_FOUND("Link for {} and {} not found"), + USER_NOT_FOUND("User with email {} not found"), + ROLE_NOT_FOUND("Role for {} not found"); + + + private String message; + + Exceptions(String message) { + this.message = message; + } + + public String getMessage() { return message; } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/org/ayfaar/app/utils/exceptions/LogicalException.java b/src/main/java/org/ayfaar/app/utils/exceptions/LogicalException.java new file mode 100644 index 00000000..f0719e0d --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/exceptions/LogicalException.java @@ -0,0 +1,22 @@ +package org.ayfaar.app.utils.exceptions; + +import org.slf4j.helpers.MessageFormatter; + +public class LogicalException extends RuntimeException { + + private ExceptionCode code; + + public LogicalException(ExceptionCode code, Object... params){ + super(MessageFormatter.arrayFormat(code.getMessage(), params).getMessage()); + this.code = code; + } + + public ExceptionCode getCode() { + return code; + } + + public void setCode(ExceptionCode code) { + this.code = code; + } +} + diff --git a/src/main/java/org/ayfaar/app/utils/hibernate/EnumHibernateType.java b/src/main/java/org/ayfaar/app/utils/hibernate/EnumHibernateType.java new file mode 100644 index 00000000..ed20d17a --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/hibernate/EnumHibernateType.java @@ -0,0 +1,101 @@ +package org.ayfaar.app.utils.hibernate; + +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.usertype.ParameterizedType; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Properties; + +public class EnumHibernateType implements UserType, ParameterizedType { + public static final String ENUM = "enum"; + public static final String CLASS = "enumClass"; + + private static final int[] VARCHAR_SQL_TYPE = new int[]{ Types.VARCHAR }; + private static final int[] CHAR_SQL_TYPE = new int[]{ Types.CHAR }; + private Class enumClass; + private boolean valueMode; + private Method getter; + private Method generator; + + public void setParameterValues(Properties parameters) { + String enumClassName = parameters.getProperty(CLASS); + + try { + enumClass = Class.forName(enumClassName).asSubclass(Enum.class); + valueMode = ValueEnum.class.isAssignableFrom(enumClass); + if (valueMode) { + getter = enumClass.getMethod("getValue"); + generator = enumClass.getMethod("getEnum", String.class); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return cached; + } + + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + public Serializable disassemble(Object value) throws HibernateException { + return (Enum) value; + } + + public boolean equals(Object x, Object y) throws HibernateException { + return x == y; + } + + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + public boolean isMutable() { + return false; + } + + public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { + String value = rs.getString(names[0]); + if (rs.wasNull() || value == null) { + return null; + } + try { + return generator != null ? generator.invoke(enumClass, value) : Enum.valueOf(enumClass, value); + } catch (Exception e) { + throw new HibernateException("invalid enum " + enumClass + " value " + value, e); + } + } + + public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { + if (value == null) { + st.setNull(index, sqlTypes()[0]); + } else { + try { + st.setString(index, getter != null ? getter.invoke(value).toString() : ((Enum) value).name()); + } catch (Exception e) { + throw new HibernateException("invalid enum " + enumClass + " object " + value, e); + } + } + } + + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return original; + } + + public Class returnedClass() { + return enumClass; + } + + public int[] sqlTypes() { + return valueMode ? CHAR_SQL_TYPE : VARCHAR_SQL_TYPE; + } +} \ No newline at end of file diff --git a/src/main/java/org/ayfaar/app/utils/hibernate/ValueEnum.java b/src/main/java/org/ayfaar/app/utils/hibernate/ValueEnum.java new file mode 100644 index 00000000..c5682719 --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/hibernate/ValueEnum.java @@ -0,0 +1,5 @@ +package org.ayfaar.app.utils.hibernate; + +public interface ValueEnum { + T getValue(); +} diff --git a/src/main/java/org/ayfaar/app/utils/servlet/AddAccessControlAllowOriginFilter.java b/src/main/java/org/ayfaar/app/utils/servlet/AddAccessControlAllowOriginFilter.java index 2fa46af5..bd760c02 100644 --- a/src/main/java/org/ayfaar/app/utils/servlet/AddAccessControlAllowOriginFilter.java +++ b/src/main/java/org/ayfaar/app/utils/servlet/AddAccessControlAllowOriginFilter.java @@ -1,13 +1,17 @@ package org.ayfaar.app.utils.servlet; +import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; +import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +@WebFilter(urlPatterns = "/api/*") +@Component /** * From http://stackoverflow.com/a/16191770/975169 */ diff --git a/src/main/java/org/ayfaar/app/utils/servlet/DefaultFilter.java b/src/main/java/org/ayfaar/app/utils/servlet/DefaultFilter.java deleted file mode 100644 index d0654df2..00000000 --- a/src/main/java/org/ayfaar/app/utils/servlet/DefaultFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.ayfaar.app.utils.servlet; - -import javax.servlet.*; -import java.io.IOException; - -public class DefaultFilter implements Filter { - - private RequestDispatcher defaultRequestDispatcher; - - @Override - public void destroy() {} - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - defaultRequestDispatcher.forward(request, response); - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - this.defaultRequestDispatcher = - filterConfig.getServletContext().getNamedDispatcher("default"); - } -} diff --git a/src/main/java/org/ayfaar/app/utils/sitemap/SiteMapGenerator.java b/src/main/java/org/ayfaar/app/utils/sitemap/SiteMapGenerator.java new file mode 100644 index 00000000..b0b1907c --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/sitemap/SiteMapGenerator.java @@ -0,0 +1,70 @@ +package org.ayfaar.app.utils.sitemap; + +import com.redfin.sitemapgenerator.WebSitemapGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.stream.Stream; + +@Service +@Slf4j +@EnableScheduling +@Controller +@RequestMapping("api") +@Profile("default") +public class SiteMapGenerator { + + @Value("${site-base-url}") + private String baseUrl; + + @Value("${sitemap-dir}") + private String sitemapDir; + + private ResourceLoader resourceLoader; + private URLGenerator urlGenerator; + + @Inject + public SiteMapGenerator(URLGenerator urlGenerator,ResourceLoader resourceLoader) { + this.urlGenerator = urlGenerator; + this.resourceLoader = resourceLoader; + } + + private void generateSiteMap(Stream urls) throws IOException { + Resource resource = resourceLoader.getResource("file:" + sitemapDir); + if (!resource.exists()) { + throw new RuntimeException("Error locating sitemap dir "+sitemapDir); + } + File sitemapDirObj = resource.getFile(); + + WebSitemapGenerator wsg = new WebSitemapGenerator(baseUrl, sitemapDirObj); + urls.forEach(s -> { + try { + wsg.addUrl(s); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }); + wsg.write(); + log.info("Sitemap location: {}", sitemapDirObj.getAbsolutePath() + "/sitemap.xml"); + } + + @Scheduled(cron="0 0 0 * * *") // every day at 0 hours + @RequestMapping("update-sitemap") + public void createSiteMap() throws IOException { + log.info("Sitemap generation started"); + generateSiteMap(urlGenerator.getURLs()); + log.info("Sitemap generation finished"); + } +} diff --git a/src/main/java/org/ayfaar/app/utils/sitemap/URLGenerator.java b/src/main/java/org/ayfaar/app/utils/sitemap/URLGenerator.java new file mode 100644 index 00000000..e9c71bed --- /dev/null +++ b/src/main/java/org/ayfaar/app/utils/sitemap/URLGenerator.java @@ -0,0 +1,82 @@ +package org.ayfaar.app.utils.sitemap; + +import com.google.common.base.Charsets; +import lombok.extern.slf4j.Slf4j; +import one.util.streamex.StreamEx; +import org.ayfaar.app.dao.ItemDao; +import org.ayfaar.app.services.topics.TopicService; +import org.ayfaar.app.utils.ContentsService; +import org.ayfaar.app.utils.TermService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriUtils; + +import javax.inject.Inject; +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.util.stream.Stream; + + +@Service +@Slf4j +public class URLGenerator { + + @Value("${site-base-url}") + private String baseUrl; + + private TermService termsMap; + private ItemDao itemDao; + private ContentsService contentsService; + private TopicService topicService; + + @Inject + public URLGenerator(TermService termsMap, ItemDao itemDao, ContentsService contentsService, TopicService topicService) { + this.termsMap = termsMap; + this.itemDao = itemDao; + this.contentsService = contentsService; + this.topicService = topicService; + } + + public StreamEx getURLs(){ + return StreamEx.of(getTermsURL()) + .append(getItemsURL()) + .append(getCategoriesURL()) + .append(getParagraphsURL()) + .append(getTopicsURL()); + } + + private Stream getTopicsURL(){ + return addBaseAndEncode(topicService.getAllNames().stream().map(s -> "t/" + s)); + } + + private Stream getCategoriesURL(){ + return addBaseAndEncode(contentsService.getAllCategories().map(c -> "c/" + c.extractCategoryName())); + } + + private Stream getParagraphsURL(){ + return addBaseAndEncode(contentsService.getAllParagraphs().map(ContentsService.ParagraphProvider::code)); + } + + private Stream getItemsURL(){ + return addBaseAndEncode(itemDao.getAllNumbers().stream()); + } + + private Stream getTermsURL(){ + return addBaseAndEncode(termsMap.getAll().stream().map(Map.Entry::getKey)); + } + + private String encodeUrl(String term){ + String encodedUrl = null; + try { + encodedUrl = UriUtils.encodePath(term, Charsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + log.error("The Character Encoding is not supported.", e); + } + return encodedUrl; + } + + private Stream addBaseAndEncode(Stream strings){ + return strings.map(s -> baseUrl + "/" + encodeUrl(s)); + } + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..e206e5c2 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,12 @@ +OPENSHIFT_EXTMYSQL_DB_HOST: 127.0.0.1 +OPENSHIFT_EXTMYSQL_DB_USERNAME: root +OPENSHIFT_EXTMYSQL_DB_PASSWORD: +OPENSHIFT_EXTMYSQL_DB_PORT: 3306 +OPENSHIFT_EXTAPP_NAME: ii +OPENSHIFT_HOMEDIR: +OPENSHIFT_DATA_DIR: '/' +OPENSHIFT_BASE_URL: 'http://ii.ayfaar.org/' + +logging: + level: + org.ayfaar: trace \ No newline at end of file diff --git a/src/main/resources/application-remote.yml b/src/main/resources/application-remote.yml new file mode 100644 index 00000000..d93d7165 --- /dev/null +++ b/src/main/resources/application-remote.yml @@ -0,0 +1,8 @@ +OPENSHIFT_EXTMYSQL_DB_HOST: 127.0.0.1 +OPENSHIFT_EXTMYSQL_DB_USERNAME: adminqbbDmai +OPENSHIFT_EXTMYSQL_DB_PASSWORD: QPJLUqFKY4AB +OPENSHIFT_EXTMYSQL_DB_PORT: 65111 +OPENSHIFT_EXTAPP_NAME: ii +OPENSHIFT_HOMEDIR: +OPENSHIFT_DATA_DIR: '/' +OPENSHIFT_BASE_URL: 'http://ii.ayfaar.org/' \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..3075c530 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,53 @@ +# DB +spring: + datasource: + url: jdbc:mysql://${OPENSHIFT_EXTMYSQL_DB_HOST}:${OPENSHIFT_EXTMYSQL_DB_PORT}/${OPENSHIFT_EXTAPP_NAME}?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&useSSL=false + username: ${OPENSHIFT_EXTMYSQL_DB_USERNAME} + password: ${OPENSHIFT_EXTMYSQL_DB_PASSWORD} + platform: org.hibernate.dialect.MySQL5Dialect + test-while-idle: true + test-on-borrow: true + validation-query: SELECT 1 + time-between-eviction-runs-millis: 5000 + min-evictable-idle-time-millis: 60000 + jackson: + property-naming-strategy: CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES + serialization-inclusion: non_empty + serialization.WRITE_DATES_AS_TIMESTAMPS: false + jpa.hibernate.ddl-auto: update + + +logging: + file: ${OPENSHIFT_DATA_DIR}logs/app.log + level: + org: warn + org.ayfaar: info +# org.springframework: warn +# pattern: +# console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" + +site-base-url: 'http://ii.ayfaar.org' +sitemap-dir: ${OPENSHIFT_DATA_DIR} +drive-dir: ${OPENSHIFT_DATA_DIR} +this-url: http://${server.address:localhost}:${server.port:8080} + +mail.user: some@gmail.com +mail.password: some + +pushbullet.key: v1Izhxi9HgremmZ8ZqEyAh0xbW7xJHLYWYujz0VEpiRR6 +pushbullet.channel: ii +google.api.key: ${GOOGLE_API_KEY:'no_key'} + +server: + session: + timeout: 2147483647 + +translation: + spreadsheet-id: '1vAMl7PTVXHuRUdGI9HlbfAIYXh6X1tcPcixI1aAJgfk' + +sync: + records: + spreadsheet-id: '1W3siNcFKJaHRSz4155iRF7Y0-pzNYswNzWjp9o3R5hI' + + +spring.mvc.media-types.manifest: text/cache-manifest diff --git a/src/main/resources/database.properties b/src/main/resources/database.properties deleted file mode 100644 index 35636b6f..00000000 --- a/src/main/resources/database.properties +++ /dev/null @@ -1,5 +0,0 @@ -database.driver=com.mysql.jdbc.Driver -database.dialect=org.hibernate.dialect.MySQL5Dialect -database.show_sql=true -database.format_sql=false -database.hibernate_hbm2ddl_auto=update diff --git a/src/main/resources/debug.properties b/src/main/resources/debug.properties deleted file mode 100644 index 73ab3000..00000000 --- a/src/main/resources/debug.properties +++ /dev/null @@ -1,6 +0,0 @@ -OPENSHIFT_MYSQL_DB_HOST=127.0.0.1 -OPENSHIFT_MYSQL_DB_PASSWORD= -OPENSHIFT_MYSQL_DB_USERNAME=root -OPENSHIFT_MYSQL_DB_PORT=3306 -OPENSHIFT_APP_NAME=ii -OPENSHIFT_HOMEDIR= \ No newline at end of file diff --git a/src/main/resources/hibernate.xml b/src/main/resources/hibernate.xml index 702f20b7..c075711e 100644 --- a/src/main/resources/hibernate.xml +++ b/src/main/resources/hibernate.xml @@ -6,12 +6,12 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> - - + @@ -19,10 +19,10 @@ - ${database.dialect} - ${database.show_sql} - ${database.format_sql} - ${database.hibernate_hbm2ddl_auto} + ${spring.datasource.platform} + false + false + update @@ -30,9 +30,7 @@ - - - + diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties deleted file mode 100644 index cc8af99d..00000000 --- a/src/main/resources/logging.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.apache.catalina.core.ContainerBase.[Catalina].level = INFO -org.apache.catalina.core.ContainerBase.[Catalina].handlers = java.util.logging.ConsoleHandler \ No newline at end of file diff --git a/src/main/resources/spring-basic.xml b/src/main/resources/spring-basic.xml index a27c414a..0654af7c 100644 --- a/src/main/resources/spring-basic.xml +++ b/src/main/resources/spring-basic.xml @@ -1,99 +1,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + + + + + + true + true + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/resources/static/WEB-INF/web.xml similarity index 81% rename from src/main/webapp/WEB-INF/web.xml rename to src/main/resources/static/WEB-INF/web.xml index 5634e21a..6c5bc40e 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/resources/static/WEB-INF/web.xml @@ -20,11 +20,19 @@ default /css/* + /old/css/* + /old/js/* + /old/lib/* + /old/images/* /js/* /libs/* /uploading/* /images/* *.html + *.xml + *.js + *.css + *.* @@ -42,13 +50,16 @@ org.springframework.web.servlet.DispatcherServlet contextConfigLocation - /WEB-INF/applicationContext.xml + + /WEB-INF/applicationContext.xml, + /WEB-INF/schedule.xml + spring - /api/* + /* diff --git a/src/main/resources/static/cache.manifest b/src/main/resources/static/cache.manifest new file mode 100644 index 00000000..970817f6 --- /dev/null +++ b/src/main/resources/static/cache.manifest @@ -0,0 +1,49 @@ +CACHE MANIFEST + +CACHE: +images/favicon.ico +lib/bootstrap/css/bootstrap.min.css +lib/bootstrap/fonts/glyphicons-halflings-regular.eot +lib/bootstrap/fonts/glyphicons-halflings-regular.ttf +lib/bootstrap/fonts/glyphicons-halflings-regular.woff +lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 +css/style.css +css/icons/style.css + +lib/angular/angular.min.js +lib/angular/angular-cookies.min.js +lib/angular/angular-resource.min.js +lib/angular/angular-sanitize.min.js +lib/angular-ui-router.min.js +lib/angular.audio.min.js +lib/ui-bootstrap/ui-bootstrap-0.11.2.min.js +lib/require/require.min.js +js/controllers/common.js +js/controllers/term.js +js/app.js + +partials/header.html +partials/knowledge-base.html +partials/term.html +partials/term-prompt.html +partials/document.html +partials/image.html +partials/item.html +partials/item-range.html +partials/modal-confirm.html +partials/paragraph.html +partials/record.html +partials/resources.html +partials/topic.html +partials/topic-prompt.html +partials/topic-selector.html +partials/topic-tree.html +partials/topics-directive.html + +images/logo.png + +/template/tooltip/tooltip-popup.html +/template/typeahead/typeahead-popup.html + +NETWORK: +* \ No newline at end of file diff --git a/src/main/resources/static/css/fonts.css b/src/main/resources/static/css/fonts.css new file mode 100644 index 00000000..feb40930 --- /dev/null +++ b/src/main/resources/static/css/fonts.css @@ -0,0 +1,37 @@ +/* @license + * MyFonts Webfont Build ID 2657698, 2013-10-03T21:23:27-0400 + * + * The fonts listed in this notice are subject to the End User License + * Agreement(s) entered into by the website owner. All other parties are + * explicitly restricted from using the Licensed Webfonts(s). + * + * You may obtain a valid license at the URLs below. + * + * Webfont: Futura Book by Bitstream + * URL: http://www.myfonts.com/fonts/bitstream/futura/book/ + * + * Webfont: Futura Medium by Bitstream + * URL: http://www.myfonts.com/fonts/bitstream/futura/medium/ + * + * Webfont: Futura Bold by Bitstream + * URL: http://www.myfonts.com/fonts/bitstream/futura/bold/ + * + * + * License: http://www.myfonts.com/viewlicense?type=web&buildid=2657698 + * Licensed pageviews: 10,000 + * Webfonts copyright: Copyright 1990-2003 Bitstream Inc. All rights reserved. + * + * © 2013 MyFonts Inc +*/ +/* @import must be at top of file, otherwise CSS will not work */ +/* @import url("//hello.myfonts.net/count/288da4"); */ + + +@font-face {font-family: 'FuturaBT-Book';src: url('/fonts/288DA2_0_0.eot');src: url('/fonts/288DA2_0_0.eot?#iefix') format('embedded-opentype');}@font-face {font-family: 'FuturaBT-Book';src:url('data:font/opentype;base64,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ACAAMQAuADAAMQAgAGUAbQBiADQALQBPAFQAOwBjAG8AbQAuAG0AeQBmAG8AbgB0AHMALgBiAGkAdABzAHQAcgBlAGEAbQAuAGYAdQB0AHUAcgBhAC4AYgBvAG8AawAuAHcAZgBrAGkAdAAyAC4AZQBDADMAcQBDAG8AcAB5AHIAaQBnAGgAdAAgADEAOQA5ADAALQAyADAAMAAzACAAQgBpAHQAcwB0AHIAZQBhAG0AIABJAG4AYwAuACAAQQBsAGwAIAByAGkAZwBoAHQAcwAgAHIAZQBzAGUAcgB2AGUAZAAuAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBtAHkAZgBvAG4AdABzAC4AYwBvAG0ALwB2AGkAZQB3AGwAaQBjAGUAbgBzAGUAPwBsAGkAZAA9ADEANgAzADAmHgBGAHUAdAB1AHIAYQAgAEIAawAgAEIAVAAgAEIAbwBvAGsARgB1AHQAdQByAGEAQgBUAC0AQgBvAG8Aa0Z1dHVyYSBCayBCVCBCb29rAAAAAAIAAAAAAAD/IQB1AAAAAAAAAAAAAAAAAAAAAAAAAAABBQAAAAEAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkAGgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0AbgBvAHAAcQByAHMAdAB1AHYAdwB4AHkAegB7AHwAfQB+AH8AgACBAIIAgwCEAIUAhgCHAIgAiQCKAIsAjACNAI4AjwCQAJEAkgCTAJQAlQCWAJcAmACZAJoAmwCcAJ0AngCfAKAAoQCiAKMApAClAKYApwCoAKkAqgCrAKwArQCuAK8AsACxALIAswC0ALUAtgC3ALgAuQC6ALsAvAC9AL4AvwDAAMEAwgEDAMQAxQDGAMcAyADJAMoAywDMAM0AzgDPANAA0QDSANMA1ADVANYA1wDYANkA2gDbANwA3QDeAN8A4ADhAOIA4wDkAOUA5gDnAOgA6QDq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/gNvuP/RQP8EbkEDbX0DbGuMBWz+A2zABGtqWQVrjANrgARqaSYFalkDakAEaSYDZf4DZP4DY/4DYv4DYf4DXn0DWi4DWP4DV30DVvoDVf4DVJYDUv4DULsDT30DTv4DTWsDS/4DRUQgBUX+A0QgA0FA/gVA/gM/PhQFP/4DPhQDOV0DOD4DN/4DNv4DNf4DNCkYBTT+AzP+AzJ9AzGWAzC7Ay/+Ay4tZAUuuwMtZAMsfQMr/gMrEAQqUwMpGAMo/gMm/gMl+gMj/gMi/gMhuwMg/gMfHiYFH/4DHiYDHf4DHP4DG/4DGv4DGQo3BRl9AxhdAxcsAxYVZAUW/gMVZAMU/gMT+gNAQQ8OJQUOJQMNCjcFDUQDDAt9BQyWAwsKNwULfQMKNwMJRAMGBSAFBv4DBSADBANkBQS7AwMCIAUDZAMCIAMA+gMAuAFkhY0BKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysAKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKx0AAAA=') format('truetype');} + + +@font-face {font-family: 'FuturaBT-Medium';src: url('/fonts/288DA2_1_0.eot');src: url('/fonts/288DA2_1_0.eot?#iefix') format('embedded-opentype');}@font-face {font-family: 'FuturaBT-Medium';src:url('data:font/opentype;base64,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ACAAMQAuADAAMQAgAGUAbQBiADQALQBPAFQAOwBjAG8AbQAuAG0AeQBmAG8AbgB0AHMALgBiAGkAdABzAHQAcgBlAGEAbQAuAGYAdQB0AHUAcgBhAC4AbQBlAGQAaQB1AG0ALgB3AGYAawBpAHQAMgAuAGUAQwAzAHEAQwBvAHAAeQByAGkAZwBoAHQAIAAxADkAOQAwAC0AMgAwADAAMwAgAEIAaQB0AHMAdAByAGUAYQBtACAASQBuAGMALgAgAEEAbABsACAAcgBpAGcAaAB0AHMAIAByAGUAcwBlAHIAdgBlAGQALgBoAHQAdABwADoALwAvAHcAdwB3AC4AbQB5AGYAbwBuAHQAcwAuAGMAbwBtAC8AdgBpAGUAdwBsAGkAYwBlAG4AcwBlAD8AbABpAGQAPQAxADYAMwAwJh4ARgB1AHQAdQByAGEAIABNAGQAIABCAFQAIABNAGUAZABpAHUAbQBGAHUAdAB1AHIAYQBCAFQALQBNAGUAZABpAHUAbUZ1dHVyYSBNZCBCVCBNZWRpdW0AAAIAAAAAAAD/IQCRAAAAAAAAAAAAAAAAAAAAAAAAAAABBQAAAAEAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkAGgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0AbgBvAHAAcQByAHMAdAB1AHYAdwB4AHkAegB7AHwAfQB+AH8AgACBAIIAgwCEAIUAhgCHAIgAiQCKAIsAjACNAI4AjwCQAJEAkgCTAJQAlQCWAJcAmACZAJoAmwCcAJ0AngCfAKAAoQCiAKMApAClAKYApwCoAKkAqgCrAKwArQCuAK8AsACxALIAswC0ALUAtgC3ALgAuQC6ALsAvAC9AL4AvwDAAMEAwgEDAMQAxQDGAMcAyADJAMoAywDMAM0AzgDPANAA0QDSANMA1ADVANYA1wDYANkA2gDbANwA3QDeAN8A4ADhAOIA4wDk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RWBELSxLU1iwAiWwAiVFRFkhIS0sRUQtQRwCgAEHAP4AAwEGAQUAEQAFAQYAyAADAQUAEQADAQQBAwAoAAUBBAD+AAMBAwAoAAMBAgD+AAMBALUcA/8oA/+4//RA7gT+awP9/gP6/gP5NQP4PgP2/gP1/gP0FAPz/gPy8YwF8v4D8sAE8fBZBfGMA/GABPDvJgXwWQPwQATvJgPufQPt+gPs+gPr+gPq/gPp/gPnUwPm/gPlSwPkqkcF5LsD4/4D3/4D3v4D3f4D3P4D2/oD2v4D2dj+Bdj+A9e7A9b+A9VLA9F6GAXRSwPP+gPOc0cFzmsDzf4DzMv+Bcv+A8rJ/gXJ/gPIxx8Fxx8Dxv4Dxf4Dvf4DvKpHBbxTA7t6GAW7SAO6uRgFuv4DuRgDuP4Dt7YyBbf+A7axFAW2MgO1e30FtZYDtLMLBbQTA7S4//BAOgSzCwOysRQFsv4DsRQDsP4Dr66UBa/+A6/ABK6tYQWulAOugAStrC4FrWEDrUAErC4Dq6pHBav+A6u4/9FAGwSqRwOpqBoFqf4DqBoDp/4Dpv4Dpf4DogoDorj//UBGBKEQA6EDBKD+A5+eeAWf+gOemxoFnngDnTUDnJsaBZyWA5saA5pdA5n6A5j+A5f+A5STGAWU/gOTGAOSc0cFklMDkQ4Dkbj/+0DJBJBnQQWQXQOPjmoFj30DjmoDjRIDjG8lBYz+A4tEA4pvJQWKNQOJ/gOI/gOH/gOG/gOFhDcFhf4DhDcDg4IOBYN9A4IOA4G7A4B/RwWA/gN/RwN+fRQFfRQDfCkDe3oYBXt9A3oYA3l4jAV5/gN5wAR4d1kFeIwDeIAEd28lBXdZA3dABHRzRwV0fQNzRwNycYwFcv4DcsAEcXBZBXGMA3GABHBvJQVwWQNwQARvJQNrapYFa/4DamkyBWqWA2kyA2hnQQVo/gNouP/RQP8EZ0EDZv4DZf4DZGMLBWQQA2RABGNiCgVjCwNjQARiCgNh/gNgawNfXv4FXv4DXP4DW1r+BVr+A1j+A1cuA1RT+gVU/gNT+gNSuwNR/gNQfQNO/gNM+gNL/gNKawNJ/gNIXQNG/gNEMgNBUwNAlgM9/gM8Ox4FOx4DODcUBTj+AzcUAzL+AzH+AzAvGAUw/gMvGAMu/gMtlgMs/gMrfQMq/gMp/gMpEAQoSwMn/gMl/gMk/gMj+gMh/gMg/gMfHmQFH7sDHgAyBR5kAx36Axz+AxsaLQUb/gMaLQMZ/gMY/gMXlgMWRAMV/gMU/gMTADIFE1MDEhBTBRJrAxEQUwVANhF9AxBTAw8OPgUP/gMOPgMNuwMJCBcFCBcDBwZTBQf6AwZTAwIBlgUC/gMBADIFAZYDADIDALgBZIWNASsrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysAKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysdAA==') format('truetype');} + + +@font-face {font-family: 'FuturaBT-Bold';src: url('/fonts/288DA2_2_0.eot');src: url('/fonts/288DA2_2_0.eot?#iefix') format('embedded-opentype');}@font-face {font-family: 'FuturaBT-Bold';src:url('data:font/opentype;base64,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ACAAMQAuADAAMQAgAGUAbQBiADQALQBPAFQAOwBjAG8AbQAuAG0AeQBmAG8AbgB0AHMALgBiAGkAdABzAHQAcgBlAGEAbQAuAGYAdQB0AHUAcgBhAC4AYgBvAGwAZAAuAHcAZgBrAGkAdAAyAC4AZQBDADMAcQBDAG8AcAB5AHIAaQBnAGgAdAAgADEAOQA5ADAALQAyADAAMAAzACAAQgBpAHQAcwB0AHIAZQBhAG0AIABJAG4AYwAuACAAQQBsAGwAIAByAGkAZwBoAHQAcwAgAHIAZQBzAGUAcgB2AGUAZAAuAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBtAHkAZgBvAG4AdABzAC4AYwBvAG0ALwB2AGkAZQB3AGwAaQBjAGUAbgBzAGUAPwBsAGkAZAA9ADEANgAzADAmHgBGAHUAdAB1AHIAYQAgAE0AZAAgAEIAVAAgAEIAbwBsAGQARgB1AHQAdQByAGEAQgBUAC0AQgBvAGwAZEZ1dHVyYSBNZCBCVCBCb2xkAAAAAAIAAAAAAAD/FwEQAAAAAAAAAAAAAAAAAAAAAAAAAAABBQAAAAEAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkAGgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgBjAGQAZQBmAGcAaABpAGoAawBsAG0AbgBvAHAAcQByAHMAdAB1AHYAdwB4AHkAegB7AHwAfQB+AH8AgACBAIIAgwCEAIUAhgCHAIgAiQCKAIsAjACNAI4AjwCQAJEAkgCTAJQAlQCWAJcAmACZAJoAmwCcAJ0AngCfAKAAoQCiAKMApAClAKYApwCoAKkAqgCrAKwArQCuAK8AsACxALIAswC0ALUAtgC3ALgAuQC6ALsAvAC9AL4AvwDAAMEAwgEDAMQAxQDGAMcAyADJAMoAywDMAM0AzgDPANAA0QDSANMA1ADVANYA1wDYANkA2gDbANwA3QDeAN8A4ADhAOIA4wDkAOUA5gDnAOgA6QDq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BdYfA9X+A9T+A9P+A9HQUwXQUwPK/gPJ/gPIDwPHLAPHuP/7QCQExv4DxW5HBcVLA8TDMgXE/gPDchQFwzIDwkQDwcAOBcEYA8G4//a0BMAOA8C4/+hAIQS/vhIFvyEDvr0OBb4SA70OA7y7DwW8/gO7ug0Fuw8Du7j/+0BUBLoNA7k1A7j+A7e2lAW3/gO3wAS2tWEFtpQDtoAEtbQrBbVhA7VABLQrA7OyFAWz/gOyFAOx/gOw/gOv/gOuhgwFrKUpBaw5A6ulKQWrlgOq/gOquP/gQCQEqXsTBam7A6h7EwWoOAOnBwOmpSkFpkQDpXsTBaUpA6QyA6S4/+BAJQSjexMFozUDoj4DofoDoH0Dn5YDnv4Dm5oYBZv+A5oYA5kLA5m4//RAXQSYlyIFl3sTBZciA5Z7EwWW/gOVexMFlRwDlP4Dk3sTBZMZA5J3JgWS+gORexMFkTUDkP4Dj/4Djv4Djf4DjCwDi5YDiolHBYr+A4lHA4iHNwWHhgwFhzcDhgwDhrj/+0AMBIX+A4R9QQWE/gOEuP/RQCcEg4KMBYP+A4PABIKBWQWCjAOCgASBdyYFgVkDgUAEfn1BBX6WA364/9FASwR9QQN8exMFfLsDexMDenmMBXr+A3rABHl4WQV5jAN5gAR4dyYFeFkDeEAEdyYDc3IUBXP+A3IUA3FwDQVx/gNwDQNvbkcFb/4Db7j/0UD/BG5HA23+A2z+A2tqRwVr/gNqRwNp/gNoMgNk/gNj/gNiLANe/gNcUwNbfQNa/gNZMgNXMgNWUwNV/gNT/gNS/gNRUwNQ/gNPawNN/gNJ/gNG+gNDQjcFQjcDQD8UBUD+Az8UAzr+AzlrAzj+Azf+AzYPAzX+AzQ+AzM1AzK7AzGWAzCWAy9EAy7+Ay2WAyz+Ayo1Ayn+Ayf+Ayb+AyX+AyP+AyIDFQUilgMhAxUFIX0DIP4DHwMVBR81Ax6WAx3+AxwbNwUc/gMbNwMaMgMZ/gMY/gMX/gMW/gMVAxUFFSIDFDIDEzIDEhFTBRL+AxFTAxD+Aw+WAwsKZAUKZAMJQB0jAwh9AwcuAwQDFQUEfQMDFQMBAA0FAf4DAA0DALgBZIWNASsrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrACsrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysdAAAA') format('truetype');} + diff --git a/src/main/webapp/css/icons/fonts/icomoon.dev.svg b/src/main/resources/static/css/icons/fonts/icomoon.dev.svg similarity index 100% rename from src/main/webapp/css/icons/fonts/icomoon.dev.svg rename to src/main/resources/static/css/icons/fonts/icomoon.dev.svg diff --git a/src/main/resources/static/css/icons/fonts/icomoon.eot b/src/main/resources/static/css/icons/fonts/icomoon.eot new file mode 100644 index 00000000..ca5a3a3f Binary files /dev/null and b/src/main/resources/static/css/icons/fonts/icomoon.eot differ diff --git a/src/main/resources/static/css/icons/fonts/icomoon.svg b/src/main/resources/static/css/icons/fonts/icomoon.svg new file mode 100644 index 00000000..1402a830 --- /dev/null +++ b/src/main/resources/static/css/icons/fonts/icomoon.svg @@ -0,0 +1,13 @@ + + + +Generated by IcoMoon + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/static/css/icons/fonts/icomoon.ttf b/src/main/resources/static/css/icons/fonts/icomoon.ttf new file mode 100644 index 00000000..1454e369 Binary files /dev/null and b/src/main/resources/static/css/icons/fonts/icomoon.ttf differ diff --git a/src/main/resources/static/css/icons/fonts/icomoon.woff b/src/main/resources/static/css/icons/fonts/icomoon.woff new file mode 100644 index 00000000..312d4874 Binary files /dev/null and b/src/main/resources/static/css/icons/fonts/icomoon.woff differ diff --git a/src/main/resources/static/css/icons/style.css b/src/main/resources/static/css/icons/style.css new file mode 100644 index 00000000..52374a29 --- /dev/null +++ b/src/main/resources/static/css/icons/style.css @@ -0,0 +1,34 @@ +@font-face { + font-family: 'icomoon'; + src:url('fonts/icomoon.eot?-aeu4ef'); + src:url('fonts/icomoon.eot?#iefix-aeu4ef') format('embedded-opentype'), + url('fonts/icomoon.woff?-aeu4ef') format('woff'), + url('fonts/icomoon.ttf?-aeu4ef') format('truetype'), + url('fonts/icomoon.svg?-aeu4ef#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + font-family: 'icomoon'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-search:before { + content: "\e036"; +} +.icon-star:before { + content: "\e093"; +} +.icon-maximize:before { + content: "\e112"; +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 00000000..f0151354 --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,300 @@ +.record-code { + font-size: 80%; +} +.nowrap { + white-space: nowrap; +} +.lightblue {color: #3e9ccc} +/*record-card { + display: block; + padding-bottom: 20px; +}*/ +.player-progress-bar { + display: table-cell; + width: 550px !important; +} +.player-bar { + display: table; + width: 100% +} +.video-play-button { + font-size: 50px; + position: absolute; + top: 15%; + left: 40%; + opacity: 0.3; +} +.record-block .h {visibility: hidden} +.record-block:hover .h {visibility: visible} +.play-button { + width: 20px; + font-size: 120%; +} +.card { + background: #fff; + border-radius: 2px; + box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12); + color: #272727; + display: block; + margin: 16px 0; + overflow: hidden; + position: relative; + vertical-align: top; + height: 200px; + width: 210px; + text-decoration: initial !important; + text-align: center; +} +.card.picture img { + width: 210px; + height: 210px; +} +.card.picture { + padding: 0; + height: 280px; +} +.card.picture.with-hint { + height: 310px; +} +.card.document { + height: 380px; +} +.card:hover { + box-shadow: 0 8px 20px 0 rgba(0,0,0,0.16),0 8px 40px 0 rgba(0,0,0,0.12); +} +.card img { + width: 210px; +} +.card.with-hint div { + padding-bottom: 2px; +} +.card div { + padding: 10px; + font-size: 90%; + color: darkblue; + /*word-break: break-all;*/ +} +.nopadding {padding: 0} +.padding, .padding-10 {padding: 10px} +.small { + font-size: small; +} +term:hover { + border-bottom: 2px dotted #035697; + text-decoration: none; +} +.modal-footer { + width: 100%; +} +.full-width { + width: 100%; +} +br { + display: block; + line-height: 200%; +} +.inline {display: inline} +.middle {vertical-align: -webkit-baseline-middle;} +.top-gap {padding-top: 10px} +a.icon-search { + padding: 0; + position: relative; +} +[ii-ref] { + cursor: pointer; + color: rgb(3, 86, 151); +} +li { + list-style-type: none; +} +div[ui-view] { + padding-top: 35px; +} +header { + -webkit-box-shadow: 0 3px 3px rgba(0,0,0,0.175); + box-shadow: 0 3px 3px rgba(0,0,0,0.175); + background-color: #127AD0; + color: white; + min-height: 30px; + margin-bottom: 15px; + position: fixed; + width: 100%; + z-index: 999; +} +header .title { + padding-top: 3px; + text-decoration: none; + color: white; + cursor: pointer; + display: block; + float: left; +} +header div { + max-width: 650px; + margin: 0 auto; +} +a.icon-search { + font-size: 150%; + float: right; + color: white; + padding-top: 3px; + cursor: pointer; +} +header:after { + clear: both; +} +aside { + background: #f0f0f0; + padding: 10px; + width: 300px; + float: right; +} +body { + margin: 0 auto; + padding-bottom: 50px; + font-size: 16px; +} +.structure-item { + color: #969696; + font-size: 90%; + display: block; +} +.highlite { + font-weight: bold; +} +.bracket { + cursor: pointer; +} +.bracket:hover { + color: rgb(0, 0, 173); +} +term { + color: rgb(3, 86, 151); + cursor: pointer; +} +.trim-info { + max-width: 350px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 15px; + position: relative; +} +.btn-icon { + padding: 3px 8px 1px; + /*font-size: 1.5em;*/ +} +blockquote>cite { + height: 30px; + cursor: pointer; + display: block; +} + +strong { + font-weight: normal; + background-color: lightyellow; +} +.dropdown-menu strong { + background-color: inherit; + font-weight: bold; +} + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Trebuchet MS', 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: #00006d; +} + +div[ui-view] { + max-width: 650px; + margin: 0 auto; +} + +blockquote { + font-size: inherit; + padding: 0 10px; +} +/* home */ +.home { + margin: 0 auto; + max-width: 500px; +} +.home h1 { + text-align: center; + font-size: 180%; + color: rgb(7, 131, 199); +} +.home .header { + margin: 0 auto 30px auto; + width: 150px; + height: 145px; + padding-top: 10px; +} +.footer { + margin: 30px auto; + text-align: center; + /*width: 420px;*/ + /*margin-left: 100px;*/ +} +/* end home */ + +.full-width { + width: 100% +} +.pointer {cursor: pointer} + +.home-search-tips {font-size: 75%} +.home-contents-block a {display: block;padding-bottom: 5px;} + +/*поднять шрифт для планшета*/ +@media screen and (max-width: 767px) { + body { + font-size: 200%; + } + div[ui-view], header .title, a.icon-search { + padding-left: 5px; + padding-right: 5px; + } + header .title { + font-size: 75%; + } + a.icon-search { + font-size: 110%; + } + .home { + padding-top: 10px; + } + .dropdown-menu li a { + padding: 20px; + } + ul.dropdown-menu { + width: 100%; + } + .btn-icon { + padding: 10px 13px 7px 12px; + } + .row { + margin-right: 0; + margin-left: 0; + } + .card { + height: auto; + width: 100%; + } + .video-play-button { + top: 25%; + } + .player-progress-bar { + width: 100% !important; + } +} +@media screen and (max-width: 500px) { + .col-xs-6 { + width: 100%; + } +} + +.gray, .grey { + color: grey; +} + diff --git a/src/main/resources/static/ext/brain.html b/src/main/resources/static/ext/brain.html new file mode 100644 index 00000000..5b68f509 --- /dev/null +++ b/src/main/resources/static/ext/brain.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + +

Отделы головного мозга. Анатомическая терминология расположения частей тела. (версия 1.3)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Отделы головного мозга

+
Лобная доля
Теменная доля
Височная доля
Затылочная доля
Ствол мозга
Таламус
+ +
Неокортекс (новый)
Лимбическая система (архикортекс - старый)
Рептильный мозг (палеокортекс - древний)
+ +
Лобная доля + +
Теменная доля + +
Височная доля + +
Затылочная доля + +
Мозжечок + +
Префронтальная кора
(самая развитая)

для сравнения мозг человека и шимпанзе
+ + + +
Цингулярная кора = поясная извилина

Передняя цингулярная кора (справа)
+ + + +
Орбитофронтальная кора
анатомично синонимична
вентромедиальной префронтальной коре
+ + + +
Центр Брока
+Центр Вернике

(Центры мозга, заведующие речью, расположены у поверхности мозга с одной стороны)
+ + + +
Энторинальная кора (жёлтая)
+ + + +
Энторинальная кора (голубой)
Таламус (жёлтый)
Свод (зелёный)
Гиппокамп (красный)
+ +
Инфралимбическая кора (зона 25)

Прелимбическая кора (зона 32) +
+ +
Таламус (жёлтый)
Ножка шишковидного тела (синий)
Эпифиз=шишковидное тело (красный)
верхнее двухолмие (зеленый)
нижнее двухолмие (оранжевый)
+ + + +
Гипоталамус (красный)

Таламус (жёлтый)
Гипоталамус (коричневый)
Гипофиз (зелёный)
+ + + +
Мамиллярные тела = Сосцевидные тела
+ +
Гипофиз + +
Амигдала =
Миндалевидное тело =
Миндалина
+ +
Стриатум = Полосатое тело
(Хвостатое ядро+
Скорлупа)
+ + + +
Хвостатое ядро + +
Гиппокамп (слева)
Свод мозга (справа)
+ + + +
Мозолистое тело
(соединяет левое и правое полушарие)
+ +
Базальные ганглии (синий + часть не показана)
+ Гиппокамп со сводом (жёлтый)
+ Амигдала (красный)
+ Мозолистое тело (зелёный)
+
+ +
Красное ядро
Черная субстанция (синий)
Гипоталамус (зеленый)
Таламус (желтый) +
+ + + +
Голубое пятно
(Locus coeruleus) +
+ + + +
Эмоциональный круг Пейпеца (Papez circuitЭнторинальная ) + + + + +
Клауструм (голубой) + +
Ретикулярная формация (зелёная)
+ + + +
Мысли
(всё области задействованы)
+ +
+

Пространственное расположение частей тела

+
+Дорсальный - спинной (верхний)
Вентральный - брюшной (нижний)

+Латеральный - боковой
+Медиальный - серединный

+Дистальный - дальний
+Проксимальный - ближний
+
+(Каудальный - хвостовой)
+(Краниальный - головной)
+
+ + + +
Дорсальный - верхний
Вентральный - нижний

+Латеральный - боковой
+Медиальный - серединный
+ +
+ + + +
+Дорсальный - верхний
Вентральный - нижний
+
+Латеральный - боковой
+Медиальный - серединный

+ +вентромедиальная "равна" или "не равна" орбитофронтальная (в разных источниках по разному)
+ +
+ + Префронтальная кора
+ +
+ +

+Список изменений +
Версия 1.3 +
добавлено +
- Поясная извилина, +- Красное ядро, +- Черная субстанция, +- свод мозга, +- центр Брока, +- центр Вернике, +- Эмоциональный круг Пейпеца, +- Инфралимбическая кора, +- Прелимбическая кора, +- Мамиллярные тела, +- Голубое пятно, +- Энторинальная кора + + + + diff --git a/src/main/resources/static/images/ajax-loader.gif b/src/main/resources/static/images/ajax-loader.gif new file mode 100644 index 00000000..e2a116c7 Binary files /dev/null and b/src/main/resources/static/images/ajax-loader.gif differ diff --git a/src/main/resources/static/images/facebook.png b/src/main/resources/static/images/facebook.png new file mode 100644 index 00000000..2a0ca403 Binary files /dev/null and b/src/main/resources/static/images/facebook.png differ diff --git a/src/main/webapp/images/favicon.ico b/src/main/resources/static/images/favicon.ico similarity index 100% rename from src/main/webapp/images/favicon.ico rename to src/main/resources/static/images/favicon.ico diff --git a/src/main/webapp/images/logo.png b/src/main/resources/static/images/logo.png similarity index 100% rename from src/main/webapp/images/logo.png rename to src/main/resources/static/images/logo.png diff --git a/src/main/resources/static/images/logo150.png b/src/main/resources/static/images/logo150.png new file mode 100644 index 00000000..024fea9d Binary files /dev/null and b/src/main/resources/static/images/logo150.png differ diff --git a/src/main/resources/static/images/logo150x100.png b/src/main/resources/static/images/logo150x100.png new file mode 100644 index 00000000..36d24817 Binary files /dev/null and b/src/main/resources/static/images/logo150x100.png differ diff --git a/src/main/resources/static/images/logo155x100.png b/src/main/resources/static/images/logo155x100.png new file mode 100644 index 00000000..4a935191 Binary files /dev/null and b/src/main/resources/static/images/logo155x100.png differ diff --git a/src/main/resources/static/images/logo16.png b/src/main/resources/static/images/logo16.png new file mode 100644 index 00000000..f0b4e3c4 Binary files /dev/null and b/src/main/resources/static/images/logo16.png differ diff --git a/src/main/resources/static/images/vk.png b/src/main/resources/static/images/vk.png new file mode 100644 index 00000000..c537a91f Binary files /dev/null and b/src/main/resources/static/images/vk.png differ diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 00000000..abe9e409 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + + Интерактивная Ииссиидиология + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ {{alert.msg}} +
+ + + + + + + + + + + + + + + diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js new file mode 100644 index 00000000..dd40adbd --- /dev/null +++ b/src/main/resources/static/js/app.js @@ -0,0 +1,1747 @@ +var hash = window.location.hash; +if (hash) { + window.location.hash = ''; + window.location.pathname = window.location.pathname + hash.replace("#?", "").replace("#", ""); +} +var app = angular.module('app', ['ui.router', 'ngResource', 'ngSanitize', 'ngCookies', 'ui.bootstrap', 'ngAudio']) + .config(function($locationProvider, $urlRouterProvider, $stateProvider, config) { + if (config.useHtml5Mode) $locationProvider.html5Mode(true).hashPrefix('!'); + $urlRouterProvider.otherwise("@"); + // +// // Now set up the states + $stateProvider + .state('logout', { + url: "/logout", + controller: function($state, auth) { + auth.logout(); + $state.goToHome() + } + }) + /*.state('home', { + url: "/{at: @?}", + templateUrl: "static/partials/home.html", + controller: HomeController, + onEnter: function($rootScope){ + $rootScope.$broadcast('home-state-entered'); + } + })*/ + .state('home', { + url: "/{at: @?}", + templateUrl: "static/partials/knowledge-base.html", + controller: KnowledgeBaseController, + onEnter: function($rootScope){ + $rootScope.$broadcast('home-state-entered'); + } + }) + .state('cabinet', { + url: "/я", + templateUrl: "static/partials/cabinet.html", + controller: CabinetController + }) + .state('document', { + url: "/document/{id}", + templateUrl: "static/partials/document.html", + controller: DocumentController + }) + .state('picture', { + url: "/picture/{id}", + templateUrl: "static/partials/image.html", + controller: ImageController + }) + .state('record', { + url: "/r/{code}", + templateUrl: "static/partials/record.html", + controller: RecordController + }) + .state('topic', { + url: "/t/{name}", + templateUrl: "static/partials/topic.html", + controller: TopicController + }) + .state('tagger', { + url: "/tagger", + templateUrl: "static/partials/tagger.html", + controller: TaggerController + }) + .state('topic-tree', { + url: "/topic-tree", + templateUrl: "static/partials/topic-tree.html", + controller: TopicTreeController + }) + .state('resource-video', { + url: "/resource/video/{id: \.*}", + controller: function ($stateParams, $state) { + $state.goToVideo({id: $stateParams.id}) + } + }) + .state('video', { + url: "/v/{id: \.*}", + templateUrl: "static/partials/resources.html", + controller: ResourcesController + }) + .state('article', { + url: "/a/:id", + templateUrl: "static/partials/article.html", + controller: ArticleController + }) + .state('item', { + url: "/{number: \\s*\\d+\\.\\d+\\s*}", + templateUrl: "static/partials/item.html", + controller: ItemController + }) + .state('item-range', { + url: "/{from:\\d+\\.\\d+}{space1:\\s*}{delimiter:[-–]}{space2:\\s*}{to:\\d+\\.\\d+}", + templateUrl: "static/partials/item-range.html", + controller: ItemRangeController + }) + .state('paragraph', { + url: "/{number:\\d+\\.\\d+\\.\\d+\\.\\d+}", + templateUrl: "static/partials/paragraph.html", + controller: ParagraphController + }) + .state('category', { + url: "/c/*name", + templateUrl: "static/partials/category.html", + controller: CategoryController + }) + .state('term', { + url: "/:name", + templateUrl: "static/partials/term.html", + controller: TermController + }); + +// window.encodeURIComponent = function(arg) { +// return originEncodeURIComponent(arg); +// } + }) + .factory("statistic", function(){ + return { + termExpandedOrCollapsed: function(term, expanded) { + if (typeof ga !== 'undefined') ga('send', 'event', expanded ? 'term-expanded' : 'term-collapsed', term); + }, + termExpandedAndHasNoShortDescription: function(term) { + if (typeof ga !== 'undefined') ga('send', 'event', 'term-expanded-and-has-no-short-description', term); + }, + termGoToDescription: function(term) { + if (typeof ga !== 'undefined') ga('send', 'event', 'term-goto-description', query + " page " + pageCounter); + }, + searchNextPageLoading: function(pageCounter, query) { + if (typeof ga !== 'undefined') ga('send', 'event', 'search-next-page-loading', query + " page " + pageCounter); + }, + lookupSelected: function (query, selectedType, selectedLabel) { + if (typeof ga !== 'undefined') ga('send', 'event', 'lookup-selected', query + "->" + selectedType + ":" + selectedLabel); + }, + recordPlayed: function (recordCode) { + if (typeof ga !== 'undefined') ga('send', 'event', 'record-played', recordCode); + }, + registerEmptyTerm: function(termName) { + if (typeof ga !== 'undefined') { + ga('send', 'event', 'no-data', termName); + } + }, + registerImageSearch: function(query) { + if (typeof ga !== 'undefined') { + ga('send', 'event', 'image-search', query); + } + }, + pageview: function(url) { + if (typeof ga !== 'undefined') { + ga('send', 'pageview', url); + } + } + } + }) + .factory("$api", function($rootScope, $state, $http, errorService, $q, config, $httpParamSerializer, $injector, modal, $cookies){ + var apiUrl = config.apiUrl; +// var apiUrl = "https://ii.ayfaar.org/api/"; + function authenticate() { + var auth = $injector.get("auth"); + if (auth.isAuthenticated()) { + var d = $q.defer(); + d.resolve(); + return d.promise; + } else { + var wasAuthenticated = $cookies.getObject("auth_provider"); + return auth.authenticate(wasAuthenticated ? null : function () { + return modal.confirm("Действие нуждается в авторизации", "Представьтесь пожалуйста системе для выполнения данного действия. Это займёт пару секунд.", "Представиться") + }) + } + } + function moderatedAction(response) { + if (response.data.error.code == "CONFIRMATION_REQUIRED") { + modal.message("Действие нуждается в подтверждении", "Данное действие будет исполнено после подтверждения модератором системы"); + return true; + } + } + var api = { + authGet: function (url, data) { + var deferred = $q.defer(); + authenticate().then(function () { + api.get(url, data).then(function (response) { + deferred.resolve(response) + }, function (error) { + deferred.reject(error) + }) + }); + return deferred.promise + }, + authPost: function (url, data) { + var deferred = $q.defer(); + authenticate().then(function () { + api.post(url, data).then(function (response) { + deferred.resolve(response) + }, function (error) { + deferred.reject(error) + }) + }); + return deferred.promise + }, + post: function(url, data) { + $rootScope.$broadcast('api-call'); + var deferred = $q.defer(); + var cache = data && typeof data._cache !== 'undefined' ? data._cache : false; + if (data) delete data._cache; + $http({ + url: apiUrl+url, + data: data, + cache: cache, + method: "POST", + headers: { 'Content-Type': "application/x-www-form-urlencoded; charset=utf-8" }, + transformRequest: $httpParamSerializer + }).then(function(response){ + deferred.resolve(response.data) + },function(response){ + if (!moderatedAction(response)) { + errorService.resolve(response.data.error); + } + deferred.reject(response); + }); + return deferred.promise; + }, + get: function(url, data, skipError) { + $rootScope.$broadcast('api-call'); + var deferred = $q.defer(); + var cache = data && typeof data._cache !== 'undefined' ? data._cache : false; + if (data) delete data._cache; + $http({ + url: apiUrl+url+serializeGet(data), + method: "GET", + cache: cache, + timeout: 300000 + }).then(function(response){ + deferred.resolve(response.data) + },function(response){ + if (!moderatedAction(response) && !skipError) { + errorService.resolve(response.data.error); + } + deferred.reject(response); + }); + return deferred.promise; + }, + item: { + get: function(number) { + return api.get("v2/item", {number: number, _cache: true}) + }, + getRange: function(from, to) { + return api.get("v2/item/range", {from: from, to: to, _cache: true}) + }, + getContent: function(numberOrUri) { + numberOrUri = numberOrUri.replace("ии:пункт:", ""); + return api.get("v2/item/"+numberOrUri+"/content") + } + }, + term: { + link: function(term1, term2, type) { + return api.authPost("link/addAlias", {term1: term1, term2: term2, type: type}); + }, + get: function(name) { + return api.get('term/', {name: name, mark: true, _cache: true}, true); + }, + getShortDescription: function(termName) { + return api.get("term/get-short-description", {name: termName, _cache: true}) + }, + getTermsInText: function(text) { + return api.post("v2/term/get-terms-in-text", {text: text}) + }, + suggest: function(q) { + return api.get("v2/term/suggest", {q: q}) + } + }, + category: { + get: function(name) { + return api.get('category', {name: name, _cache: true}); + } + }, + article: { + get: function(id) { + return api.get('article/'+id, {_cache: true}); + } + }, + search: { + term: function(query) { + return api.get("search/term", {query: query}) + }, + content: function(query, startFrom, page) { + return api.get("v2/search", { + query: query, + startFrom: startFrom ? startFrom : "", + pageNumber: page + }) + }, + suggestions: function(query) { + return api.get("suggestions/"+query) + }, + suggestionsTerm: function (q) { + return api.get("suggestions/term", {q: q}) + }, + suggestionsAll: function (q) { + return api.get("suggestions/all", {q: q}) + }, + quote: function(uri, term, quote) { + return api.authPost("search/rate/+", {uri: uri, query: term, quote: quote}) + } + }, + resource: { + video: { + add: function (url) { + return api.authPost("resource/video", {url: url}) + }, + last: function (page, size) { + var params = {page: page}; + if (size) params.size = size; + return api.get("resource/video/last-created", params) + }, + updateCode: function (id, code) { + return api.authPost("resource/video/update-code", {id: id, code: code}) + } + } + }, + topic: { + last: function (size) { + return api.get("topic/last", {size: size}) + }, + suggest: function (q) { + return api.get("topic/suggest", {q: q}) + }, + getFor: function (uri) { + return api.get("topic/for/"+uri) + }, + merge: function (main, mergeInto) { + return api.authGet("topic/merge", {main: main, mergeInto: mergeInto}) + }, + updateComment: function (forUri, topicName, comment) { + return api.authPost("topic/update-comment", {forUri: forUri, name: topicName, comment: comment}) + }, + updateRate: function (forUri, topicName, rate) { + return api.authPost("topic/update-rate", {forUri: forUri, name: topicName, rate: rate}) + }, + addFor: function (objectUri, topicName, comment, rate) { + return api.authPost("topic/for", {name: topicName, uri: objectUri, comment: comment, rate: rate}) + }, + unlinkUri: function (objectUri, topicUri) { + return api.authPost("topic/unlink-uri", {topicUri: topicUri, uri: objectUri}) + }, + unlink: function (main, linked) { + return api.authPost("topic/unlink", {name: main, linked: linked}) + }, + suggest: function (q) { + return api.get("topic/suggest", {q: q}) + }, + /** + * @param name имя топика + * @param includeResources true|false + * @returns топик с родительскими и дочерними топиками, и ресурсами если требуется + */ + get: function (name, includeResources) { + return api.get("topic", {name: name, includeResources: includeResources ? "true" : "false"}) + }, + addChild: function (parent, child) { + return api.authGet("topic/add-child", {name: parent, child: child}) + } + }, + document: { + rename: function (uri,name) { + return api.authPost("document/update-name", {uri:uri, title: name}) + }, + get: function (id) { + return api.get("document/"+id) + }, + add: function (url) { + return api.authPost("document", {url: url}) + }, + last: function (page, size) { + var data = {page: page ? page : 0}; + if (size) data.size = size; + return api.get("document/last", data) + } + }, + picture: { + rename: function (uri,name) { + return api.authPost("image/update-name", {uri:uri, title: name}) + }, + get: function (id) { + return api.get("image/"+id) + }, + add: function (url) { + return api.authPost("image", {url: url}) + }, + updateComment: function (forUri, comment) { + return api.authPost("image/update-comment", {uri: forUri, comment: comment}) + }, + last: function (page, size) { + var data = {page: page ? page : 0}; + if (size) data.size = size; + return api.get("image/last", data) + }, + search: function (query){ + return api.get("image/search", {q: query}) + } + }, + record: { + rename: function (code, name) { + return api.authPost("record/"+code+"/rename", {name: name}) + }, + last: function (page, size) { + var data = {page: page ? page : 0}; + if (size) data.size = size; + return api.get("record", data) + }, + get: function (page, nameOrCode, year, kind, withUrl) { + var data = {page: page ? page : 0}; + if (nameOrCode) data.nameOrCode = nameOrCode; + if (year) data.year = year; + if (kind) data.kind = kind; + if (withUrl) data.with_url = withUrl; + return api.get("record", data) + } + }, + auth: { + registrate: function (user) { + return api.post("auth", user) + } + }, + user: { + getCurrent: function () { + return api.get("user/current") + }, + rename: function (newName) { + return api.authPost("user/current/rename", {name: newName}) + }, + hideActionsBefore: function (id) { + return api.authPost("user/hide-actions-before/"+id); + } + }, + moderation: { + pendingActions: function () { + return api.authGet("moderation/pending_actions") + }, + lastActions: function (page, size) { + var data = {page: page ? page : 0}; + if (size) data.size = size; + return api.authGet("moderation/last_actions", data) + }, + confirm: function (id) { + return api.authGet("moderation/"+id+"/confirm") + }, + cancel: function (id) { + return api.authPost("moderation/"+id+"/cancel") + } + } + }; + function serializeGet(obj) { + var str = []; + for(var p in obj) { + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + } + return str.length ? "?"+str.join("&") : ""; + } + return api; + }) + .factory('entityService', function(){ + var service = { + getName: function (uriOrObject) { + var uri, object; + uriOrObject = uriOrObject.hasOwnProperty("resource") ? uriOrObject.resource : uriOrObject; + if (uriOrObject.hasOwnProperty("uri")) { + object = uriOrObject; + uri = object.uri; + } else { + uri = uriOrObject; + } + switch (service.getType(uri)) { + case 'topic': + return uri.replace("тема:", ""); + case 'term': + return uri.replace("ии:термин:", ""); + case 'item': + return uri.replace("ии:пункт:", ""); + case 'category': + case 'categoryG': //глава + case 'categoryR': //раздел + case 'categoryT': //том + return uri.replace("категория:", ""); + case 'article': + return uri.replace("статья:", ""); + case 'paragraph': + return uri.replace("ии:пункты:", ""); + case 'record': + return uri.replace("запись:", ""); + case 'document': + return uri.replace("документ:", ""); + case 'picture': + return uri.replace("изображение:", ""); + case 'video': + return object ? object.title : uri; + } + }, + getType: function(uri) { + uri = uri.hasOwnProperty("resource") ? uri.resource : uri; + uri = uri.hasOwnProperty("uri") ? uri.uri : uri; + if (uri.indexOf("тема:") === 0) { + return 'topic' + } + if (uri.indexOf("ии:термин:") === 0) { + return 'term' + } + if (uri.indexOf("ии:пункт:") === 0) { + return 'item' + } + if (uri.indexOf("категория:Том") === 0) { + return 'categoryT' + } + if (uri.indexOf("Глава") >= 0) { + return 'categoryG' + } + if (uri.indexOf("/Раздел") >= 0) { + return 'categoryR' + } + if (uri.indexOf("категория:") === 0) { + return 'category' + } + if (uri.indexOf("статья:") === 0) { + return 'article' + } + if (uri.indexOf("видео:") === 0) { + return 'video' + } + if (uri.indexOf("документ:") === 0) { + return 'document' + } + if (uri.indexOf("изображение:") === 0) { + return 'picture' + } + if (uri.indexOf("ии:пункты:") === 0) { + return 'paragraph' + } + if (uri.indexOf("запись:") === 0) { + return 'record' + } + }, + getTypeLabel: function(uri) { + switch (service.getType(uri)) { + case 'topic': + return "тема"; + case 'term': + return "термин"; + case 'item': + return "абзац"; + case 'category': + return "оглавление"; + case 'categoryR': + return "раздел"; + case 'categoryT': + return "том"; + case 'categoryG': + return "глава"; + case 'article': + return "статья"; + case 'paragraph': + return "параграф"; + case 'video': + return "видео"; + case 'document': + return "статья"; + case 'picture': + return "изображение"; + case 'record': + return "аудио"; + } + } + }; + return service; + }) + .directive('entity', function($state, entityService) { + return { + restrict: 'E', + replace: true, + transclude: true, + template: '', + compile : function(element, attr, linker) { + return function ($scope, $element, $attr) { + var entity = $scope[$attr.ngModel]; +// var uiSref = "term({name:'"+name+"'})"; +// $attr.$set('uiSref', uiSref); + $element.append(entity.hasOwnProperty("name") ? entity.name : entityService.getName(entity)); + $element.bind('click', function() { + $state.go(entity) + }) + } + } + /*link: function(scope, element, attrs) { + var entity = scope[attrs.ngModel]; + var name = entity.uri.replace("ии:термин:", ""); + var uiSref = "term({name:'"+name+"'})"; + attrs.$set('uiSref', uiSref); + element.removeAttr('ng-transclude'); + element.append(name); + $compile(element)(scope); + }*/ + }; + }) + .service('audioPlayer', function($rootScope, ngAudio, statistic) { + return { + playOrPause: function (record) { + if (record.played) { + $rootScope.audio.pause(); + record.played = false; + return; + } + if ($rootScope.currentPlayed) $rootScope.currentPlayed.played = false; + var volume = 0.5; + if ($rootScope.audio) { + volume = $rootScope.audio.volume; + $rootScope.audio.stop(); + } + $rootScope.audio = ngAudio.load(record.url ? record.url : record.audio_url); + $rootScope.audio.volume = volume; + $rootScope.audio.play(); + record.played = true; + $rootScope.currentPlayed = record; + statistic.recordPlayed(record.code); + } + } + }) + .service('messager', function($rootScope, $timeout) { + $rootScope.alerts = []; + $rootScope.closeAlert = function(index) { + $rootScope.alerts.splice(index, 1); + }; + /*$rootScope.$on('api-call', function() { + angular.forEach($rootScope.alerts, function (alert, index) { + if (alert.type == 'success') $rootScope.closeAlert(index) + }) + });*/ + return { + ok: function (msg) { + var index = $rootScope.alerts.push({msg: msg, type: 'success'}); + $timeout(function () { + $rootScope.closeAlert(index) + }, 3000) + }, + error: function (msg) { + $rootScope.alerts.push({msg: msg, type: 'danger'}); + } + } + }) + .service('modal', function($modal) { + return { + confirm: function (title, text, action) { + return $modal.open({ + templateUrl: 'static/partials/modal-confirm.html', + controller: function ($scope, $modalInstance) { + $scope.title = title; + $scope.text = text; + $scope.action = action; + $scope.act = function() { + $modalInstance.close(); + }; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + } + }).result; + }, + prompt: function (title, text, action) { + return $modal.open({ + templateUrl: 'modal-prompt.html', + controller: function ($scope, $modalInstance) { + $scope.title = title; + $scope.text = text; + $scope.action = action; + $scope.act = function() { + $modalInstance.close($scope.text); + }; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + } + }).result; + }, + message: function (title, text) { + return $modal.open({ + templateUrl: 'modal-message.html', + controller: function ($scope, $modalInstance) { + $scope.title = title; + $scope.text = text; + $scope.ok = function() { + $modalInstance.close(); + }; + } + }).result; + } + } + }) + .service('auth', function($modal, $api, $rootScope, $q, modal, $cookies) { + return { + logout: function () { + delete $rootScope.user + }, + isAuthenticated: function () { + return typeof $rootScope.user !== 'undefined' ? $rootScope.user : false; + }, + authenticate: function (runBeforeOpenAuthWindow) { + var deferred = $q.defer(); + if ($rootScope.user) + deferred.resolve($rootScope.user); + else + $api.user.getCurrent().then(function (user) { + if (user) { + $rootScope.user = user; + deferred.resolve(user); + } + else if (runBeforeOpenAuthWindow) { + runBeforeOpenAuthWindow().then(loadHelloAndOpenModal); + } + else { + loadHelloAndOpenModal(); + } + }); + + function loadHelloAndOpenModal() { + if (typeof hello === 'undefined') { + requirejs(["static/lib/hello/hello.min.js"], function (hello) { + hello.init({ + facebook: "917074828411840", + vk: "5371182", + google: "" + }, {redirect_uri: "/static/lib/hello/redirect.html"}); + + openAuthModal() + }); + } else { + openAuthModal() + } + } + function helloAuthenticate(provider) { + var deferred = $q.defer(); + hello(provider).login({force: false, scope: 'email'}).then(function (auth) { + hello(provider).api('me').then(function (user) { + user = angular.merge({ + access_token: auth.authResponse.access_token, + auth_provider: auth.network + }, user); + deferred.resolve(user); + }); + }); + return deferred.promise; + } + + function openAuthModal() { + var provider = $cookies.getObject("auth_provider"); + if (provider) { + helloAuthenticate(provider).then(function (user) { + $api.auth.registrate(user).then(function (user) { + $rootScope.user = user; + deferred.resolve(user); + }); + }); + return + } + + $modal.open({ + templateUrl: 'modal-auth.html', + controller: function ($scope, $modalInstance) { + $scope.authenticate = function(provider) { + helloAuthenticate(provider).then(function (user) { + $scope.user = user; + // $scope.$apply(); + $cookies.putObject("auth_provider", provider); + }); + }; + $scope.registrate = function () { + $api.auth.registrate($scope.user).then(function () { + $rootScope.user = $scope.user; + $modalInstance.close($scope.user); + deferred.resolve($scope.user); + }); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + deferred.reject(); + }; + } + }) + } + return deferred.promise; + } + } + }) + .service("errorService", function(messager){ + return { + resolve: function(error) { + var message = "Неизвестная ошибка"; + if (error) { + message = error.message; + switch (error.code) { + case "ACCESS_DENIED": + message = "Представьтесь пожалуйста"; + break; + case "USER_NOT_FOUND": + message = "Пользователь не найден"; + break; + case "TOPIC_NOT_FOUND": + message = "Тема не найдена"; + break; + case "PASSWORD_NOT_VALID": + message = "Пароль неверный"; + break; + case "BAD_CREDENTIALS": + message = "Неверные email и пароль"; + break; + case "EMAIL_DUPLICATION": + message = "Такой email уже зарегистрирован в системе"; + break; + } + } + messager.error(message); + } + }; + }) + .service('$topicPrompt', function($api, $modal, $topicSelector) { + return { + prompt: function (defaultText) { + return $modal.open({ + templateUrl: 'static/partials/topic-prompt.html', + controller: function ($scope, $modalInstance) { + if (defaultText) $scope.topic = defaultText; + $scope.suggestTopics = function (q) { + return $api.topic.suggest(q); + }; + $scope.select = function() { + $modalInstance.close($scope.topic); + }; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + $scope.openSelector = function () { + $topicSelector.select().then(function (topicName) { + $modalInstance.close(topicName); + }); + }; + } + }).result; + } + } + }) + .service('$termPrompt', function($api, $modal, $topicSelector) { + return { + prompt: function (defaultText) { + return $modal.open({ + templateUrl: 'static/partials/term-prompt.html', + controller: function ($scope, $modalInstance) { + if (defaultText) $scope.term = defaultText; + $scope.suggestTerms = function (q) { + return $api.term.suggest(q); + }; + $scope.select = function() { + $modalInstance.close($scope.term); + }; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + } + }).result; + } + } + }) + .service('$topicSelector', function($api, $modal) { + return { + select: function () { + return $modal.open({ + templateUrl: 'static/partials/topic-selector.html', + controller: function ($scope, $modalInstance) { + $scope.history = []; + $scope.select = function () { + $modalInstance.close($scope.name); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + $scope.load = function (topicName, dontSaveHistory) { + if ($scope.name && !dontSaveHistory) $scope.history.push($scope.name); + $scope.children = []; + $api.topic.get(topicName).then(function (topic) { + copyObjectTo(topic, $scope); + }); + }; + $scope.back = function () { + if ($scope.history.length) + $scope.load($scope.history.pop(), true); + }; + $scope.load("классификаторы"); + } + }).result + } + } + }) + .service('$pager', function ($q) { + var obj = { + /*create: function (dataLoader) { + return { + load: function(page) { + var list = dataLoader(page); + + } + } + },*/ + createGroupedByDate: function (dataLoader, dateField, pageSize) { + var pager = { + listCollector: [], + loadNext: function () { + var deferred = $q.defer(); + dataLoader(Math.ceil(pager.listCollector.length / pageSize)).then(function (list) { + pager.listCollector.append(list); + var grouped = groupByDate(pager.listCollector, dateField); + deferred.resolve({grouped: grouped, last: list.length < pageSize, ungroupedList: pager.listCollector}); + }, function (error) { + deferred.reject(error) + }); + return deferred.promise; + }, + reset: function () { + pager.listCollector = []; + } + }; + return pager; + } + }; + return obj; + }) + .directive('iiLookup', function($api, $state, $parse, $q, entityService, statistic) { + return { + require:'ngModel', + link: function (originalScope, element, attrs, modelCtrl) { + var data; + var query; + var termOnly = Boolean(attrs.termOnly); + originalScope.$getSuggestions = function(q) { + query = q; + var deferred = $q.defer(); + if (termOnly) { + $api.search.suggestionsTerm(q).then(function (response) { + deferred.resolve(response) + }, function (response) { + deferred.reject(response) + }); + } else { + $api.search.suggestionsAll(q).then(function (response) { + data = []; + for (var uri in response) { + if (response.hasOwnProperty(uri)) + data.push({ + uri: uri, + label: response[uri], + type: entityService.getType(uri) + // typeLabel: entityService.getTypeLabel(uri) + }) + } + deferred.resolve(data) + }, function (response) { + deferred.reject(response) + }); + } + return deferred.promise; + }; + originalScope.$selected = function(item, model, label) { + $state.go(item.uri); + statistic.lookupSelected(query, item.type, item.label) + }; + var onEnter = $parse(attrs.onEnter); + element.bind('keyup', function(event) { + if (event.keyCode == 13) {// enter + if (!termOnly) internalOnEnter(); + if (onEnter) onEnter(originalScope); + } + }); + function find() { + $state.goToTerm(query) + } + function internalOnEnter() { + if (data && data.length) { + $state.go(data[0].uri) + } else { + find() + } + } + originalScope.$find = find; + } + }; + }) + .directive('iiRef', function($state, entityService, $parse) { + return { + link: function (scope, element, attrs, modelCtrl) { + var getter = $parse(attrs.iiRef); + var obj = getter(scope);//[attrs.iiRef]; + if (!obj) return; + element.attr('href', getUrl(obj)); + var label = obj.hasOwnProperty("name") ? obj.name : entityService.getName(obj); + if (entityService.getType(obj) == 'paragraph') { + label = obj.from + "-" + obj.to; + } + obj._label = label; + element.bind('click', function() { + $state.go(obj) + }) + } + }; + }) + .directive('topicRef', function($state, entityService, $parse) { + return { + link: function (scope, element, attrs, modelCtrl) { + var getter = $parse(attrs.topicRef); + var topicName = getter(scope); + if (!topicName) return; + if (topicName.hasOwnProperty("name")) topicName = topicName.name; + if (!topicName) return; + element.attr('href', "t/"+topicName); + if (!element[0].innerText) element[0].innerText = topicName; + element.bind('click', function() { + $state.goToTopic(topicName) + }) + } + }; + }) + .directive('term', function($api, statistic) { + return { + restrict: 'E', + compile : function(element, attr, linker) { + return function ($scope, $element, $attr) { + var term = $attr.id.replace("", "").replace("", "").trim(); + var primeTerm = $attr.title; + var originalContent = $element.html(); + var expanded; + var hasShortDescription = $attr.hasShortDescription; +// $attr.title= "eee"; + $element.bind('click', function(e) { + var more = e.target.tagName == "A"; + var moreAfterPrimeTerm = e.target.id == "+"; + + if (more && !moreAfterPrimeTerm) { + window.open(term, '_blank'); + //$state.goToTerm(term); + statistic.termGoToDescription(term) + return; + } + if (expanded && !moreAfterPrimeTerm) { + $element.html(originalContent); + expanded = false; + } else if (primeTerm && !moreAfterPrimeTerm) { + expanded = true; + $element.append(" ("+primeTerm+" детальнее)"); +// $element.append(" ("+primeTerm+")"); +// $compile($element.contents())($scope); + } else { + expanded = true; + if (hasShortDescription) { + var loadingPlaceHolder = " (загрузка...)"; + $element.append(loadingPlaceHolder); + $api.term.getShortDescription(moreAfterPrimeTerm ? primeTerm : term).then(function (shortDescription) { + $element.html(originalContent + " (" + shortDescription + + " детальнее)"); + }); + } else { + $element.html(originalContent + " (нет короткого пояснения, детально)"); + statistic.termExpandedAndHasNoShortDescription(term) + } + } + statistic.termExpandedOrCollapsed(term, expanded) + }) + } + } + }; + }) + .directive('uri', function(entityService) { + return { + restrict: 'E', + compile : function(element, attr, linker) { + return function ($scope, $element, $attr) { + var uri = $element.html(); + var label = $attr.label; + $element.html(""+(label ? label : entityService.getName(uri)) + ""); + } + } + }; + }) + .directive('topic', function(entityService) { + return { + restrict: 'E', + compile : function(element, attr, linker) { + return function ($scope, $element, $attr) { + var topicName = $element.html().replace("тема:", ""); + $element.html(""+topicName + ""); + } + } + }; + }) + .directive("iiBind", function($compile) { + // inspired by http://stackoverflow.com/a/25516311/975169 + return { + link: function(scope, element, attrs) { + scope.$watch(attrs.iiBind, function(newval) { + if (!newval) { + element.html(newval); + return; + } + newval = newval.replace(/(?:\r\n|\r|\n)/g, '
'); + newval = newval.replace(new RegExp("\\(([^\\)]+)\\)","gm"), "$1"); + element.html(newval); + $compile(element.contents())(scope); + }); + } + }; + }) + .directive("iiBindLite", function($compile) { + // inspired by http://stackoverflow.com/a/25516311/975169 + return { + link: function(scope, element, attrs) { + scope.$watch(attrs.iiBindLite, function(newval) { + element.html(newval); + if (newval) { + $compile(element.contents())(scope); + } + }); + } + }; + }) + .directive("contributeButton", function($modal, $api) { + return { + template: '', + scope: { + uri: "=", + text: "=" + }, + link: function(scope, element, attrs) { + element.bind('click', function(e) { + if (!scope.text && !getSelectionText()) { + alert("Выберите текст"); + } else + $modal.open({ + templateUrl: 'contribute-form.html', + controller: function ($scope, $modalInstance) { + var selectedText = getSelectionText(); + $scope.text = selectedText ? selectedText : String(scope.text).replace(/<[^>]+>/gm, ''); + $scope.quote = function () { + if (!$scope.term) { + alert("Укажите термин"); + return; + } + $api.search.quote(scope.uri, $scope.term, $scope.text).then(function () { + $modalInstance.close(); + }); + }; + $scope.link = function (type) { + if (!$scope.term || !$scope.text) return; + return $api.term.link($scope.term, $scope.text, type).then(function () { + $modalInstance.close(); + }); + } + } + }); + }) + } + } + }) + .directive("bracket", function($compile) { + return { + restrict: 'E', + scope: {}, + compile: function (element, attr, linker) { + return function ($scope, $element, $attr) { + var bracketSpan = ""; + // предотвращение глюка вставки HTML + //var html = $element.html().replace("", "").replace("", ""); + var html = "" + +bracketSpan+"(" + +"..." + +""+$element.html()+"" + +bracketSpan+")" + +""; + $element.html(""); + $element.append($compile(html)($scope)); + } + } + } + }) + .factory('focus', function($timeout) { + return function(id) { + // timeout makes sure that is invoked after any other event has been triggered. + // e.g. click events that need to run before the focus or + // inputs elements that are in a disabled state but are enabled when those events + // are triggered. + $timeout(function() { + var element = document.getElementById(id); + if(element) + element.focus(); + }); + }; + }) + .factory('poster', function($httpParamSerializer) { + return { + action: "save", + method: "POST", + isArray: false, + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + transformRequest: $httpParamSerializer + } + }) + .factory('Video', function($resource, config, poster) { + poster.interceptor = {responseError: function(response) { + return response.data; + }}; + return $resource(config.apiUrl + "resource/video", {}, { + save: poster, + get: { + method: "GET", + isArray: false, + url: config.apiUrl + "resource/video/:id" + } + }); + }) + .directive('youtube', function($sce) { + return { + restrict: 'EA', + scope: { code:'=' }, + replace: true, + template: '
', + link: function (scope) { + scope.$watch('code', function (newVal) { + if (newVal) { + scope.url = $sce.trustAsResourceUrl("http://www.youtube.com/embed/" + newVal); + } + }); + } + }; + }) + .directive('googleDoc', function($sce) { + return { + restrict: 'EA', + scope: { id: '=' }, + replace: true, + template: '
', + link: function (scope) { + scope.$watch('id', function (id) { + if (id) { + scope.url = $sce.trustAsResourceUrl("https://drive.google.com/file/d/"+id+"/preview"); + } + }); + } + }; + }) + .directive("iiHeader", function($rootScope, focus, $state, $timeout) { + return { + scope: {}, + templateUrl: "static/partials/header.html", + link: function(scope, element, attrs) { + scope.visible = true; + scope.expand = function() { + scope.expanded = true; + focus('search-input'); + scope.query = getSelectedText(); + }; + scope.search = function() { + scope.$suggestionSelected(scope.query); + scope.expanded = false; + }; + scope.closeWithDelay = function() { + scope.focused = false; + $timeout(function(){ + if (!scope.focused) scope.expanded = false; + }, 5000, true); + }; + $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){ + scope.$root.hideLoop = false; + if (toState.name != "home") { + scope.visible = true; + } + }); + $rootScope.$on('home-state-entered', function() { + scope.$root.hideLoop = true; + scope.visible = false; + }); + $rootScope.$watch('audio', function () { + scope.audio = $rootScope.audio; + }); + scope.playPause = function () { + scope.audio.play() + } + } + }; + }) + .directive("topics", function($topicSelector, $api, modal) { + return { + scope: { ownerUri: '='}, + templateUrl: "static/partials/topics-directive.html", + link: function(scope, element, attrs) { + scope.updateRate = function(topic){ + $api.topic.updateRate(scope.ownerUri, topic.name, topic.rate).then(getTopics); + }; + scope.updateComment = function(topic){ + modal.prompt("Редактирование комментария", topic.comment, "Сохранить").then(function (comment) { + $api.topic.updateComment(scope.ownerUri, topic.name, comment).then(getTopics); + }); + }; + scope.addTopic = function () { + if (!scope.newTopic.name) return; + $api.topic.addFor(scope.ownerUri, scope.newTopic.name, scope.newTopic.comment, scope.newTopic.rate) + .then(function(topic){ + scope.newTopic = {}; + getTopics(); + }, function () { + scope.newTopic = {}; + }); + }; + scope.removeTopic = function (topic) { + modal.confirm("Подтверждение", "Комментарий и оценка будут утеряны. Вы уверены что желаете убрать тему?", "Убрать тему") + .then(function () { + $api.topic.unlinkUri(scope.ownerUri, topic.uri).then(getTopics); + }) + }; + scope.getSuggestions = function (q) { + return $api.topic.suggest(q) + }; + function getTopics() { + if (!scope.ownerUri) return; + scope.loading = true; + $api.topic.getFor(scope.ownerUri).then(function(topics){ + angular.forEach(topics, function (topic) { + topic.isItem = isItemNumber(topic.name) + }); + scope.topics = topics; + })['finally'](function () { + scope.loading = false; + }); + } + scope.openSelector = function () { + $topicSelector.select().then(function (topicName) { + scope.newTopic.name = topicName; + }); + }; + scope.$watch('ownerUri', getTopics); + scope.newTopic = {} + } + }; + }) + .directive('loadingIndicator', function() { + return { + template: '
Загрузка...
' + } + }) + .directive('topicTreeNode', function() { + return { + scope: { node: '=', expand: '='}, + template: + '
' + + '
' + + '' + + ' ' + + '{{node.name}}' + + '
' + + ' ' + + '
' + } + }) + .directive('parents', function(entityService) { + return { + template: '' + + '{{parent._label}}{{$last ? "" : "/"}}' + } + }) + .directive("videoCard", function () { + return { + scope: { video: '='}, + templateUrl: "card-video" + } + }) + .directive("documentCard", function () { + return { + scope: { doc: '='}, + templateUrl: "card-document" + } + }) + .directive("recordCard", function ($rootScope, $topicPrompt, $api, ngAudio, $parse, audioPlayer, $timeout) { + return { + scope: { record: '=', excludeTopic: '='}, + templateUrl: "record-card", + link: function(scope, element, attrs) { + if (scope.record.hasOwnProperty('resource')) { + var topics = []; + angular.forEach(scope.record.topics, function(topic) { + if (topic != scope.excludeTopic) topics.push(topic); + }); + var uri = scope.record.resource.uri; + scope.record = scope.record.resource; + scope.record.topics = topics; + scope.record.uri = uri; + } + scope.playOrPause = function(record) { + audioPlayer.playOrPause(record); + document.title = record.name; + }; + + scope.addTopic = function (record) { + $topicPrompt.prompt().then(function (topic) { + $api.topic.addFor(record.uri, topic).then(function () { + if (scope.$parent.$parent.update) scope.$parent.$parent.update(); + }) + }); + } + } + } + }) + .run(function($state, entityService, $rootScope, statistic, modal, $timeout){ + var originStateGo = $state.go; + $state.go = function(to, params, options) { + if (to.hasOwnProperty('uri') || angular.isString(to)) { + var uri = angular.isString(to) ? to : to.uri; + switch (entityService.getType(uri)) { + case "term": + originStateGo.bind($state)("term", {name: entityService.getName(uri)}); + return; + case "topic": + originStateGo.bind($state)("topic", {name: entityService.getName(uri)}); + return; + case "item": + var number = entityService.getName(uri); + if (isTom6(number)) return; + originStateGo.bind($state)("item", {number: number}); + return; + case "category": + case "categoryR": //раздел + case "categoryT": //том + case "categoryG": //глава + originStateGo.bind($state)("category", {name: entityService.getName(uri)}); + return; + case "paragraph": + var number = entityService.getName(uri); + if (isTom6(number)) return; + originStateGo.bind($state)("paragraph", {number: number}); + return; + case "article": + originStateGo.bind($state)("article", {id: entityService.getName(uri)}); + return; + case "record": + originStateGo.bind($state)("record", {code: entityService.getName(uri)}); + return; + case "video": + var id = to.id; + if (!id) { + id = uri.replace("видео:youtube:", "") + } + originStateGo.bind($state)("video", {id: id}); + return; + case "document": + var id = to.id; + if (!id) { + id = uri.replace("документ:google:", "") + } + originStateGo.bind($state)("document", {id: id}); + return; + case "picture": + var id = to.id; + if (!id) { + id = uri.replace("изображение:", "") + } + originStateGo.bind($state)("picture", {id: id}); + return; + } + } else { + originStateGo.bind($state)(to, params, options) + } + }; + $state.goToVideo = function(video) { + originStateGo.bind($state)("video", {id: video.id}) + }; + $state.goToDoc = function(doc) { + originStateGo.bind($state)("document", {id: doc.id}) + }; + $state.goToImg = function(img) { + originStateGo.bind($state)("picture", {id: img.id}) + }; + $state.goToMainTopic = function() { + $state.goToTopic("классификаторы"); + }; + $state.goToTopic = function(topicName) { + originStateGo.bind($state)("topic", {name: topicName}) + }; + $state.goToItem = function(number) { + originStateGo.bind($state)("item", {number: number}) + }; + $state.goToItemRange = function(from, to) { + originStateGo.bind($state)("item-range", {from: from, to: to}) + }; + $state.goToTerm = function(name) { + originStateGo.bind($state)("term", {name: name}) + }; + $state.goToHome = function() { + originStateGo.bind($state)("home") + }; + $state.goToCabinet = function() { + originStateGo.bind($state)("cabinet") + }; + + $state.redirectToItem = function(item) { + window.location.replace(item); + }; + $state.redirectTo = function(url) { + window.location.replace(url); + }; + + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ + $timeout(function(){statistic.pageview(location.pathname)}, 1000); + }); + function isTom6(number) { + if (number.indexOf("6.") == 0) { + modal.message("", "6 том пока официально не опубликован, поэтому его текста нет в системе"); + return true; + } + } + }) + .filter('cut', function () { + return function (value, wordwise, max, tail) { + if (!value) return ''; + + max = parseInt(max, 10); + if (!max) return value; + if (value.length <= max) return value; + + value = value.substr(0, max); + if (wordwise) { + var lastspace = value.lastIndexOf(' '); + if (lastspace != -1) { + value = value.substr(0, lastspace); + } + } + + return value + (tail || '…'); + }; + }) + .filter('highlight', function($sce) { + return function(text, phrase) { + if (phrase && text) text = text.replace(new RegExp('('+phrase+')', 'gi'), + '$1'); + return $sce.trustAsHtml(text) + } + }) + .filter('duration', function() { + var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/; + var DURATION_FORMATS = { + y: { // years + // "longer" years are not supported + value: 365 * 24 * 60 * 60 * 1000, + }, + yy: { + value: 'y', + pad: 2, + }, + d: { // days + value: 24 * 60 * 60 * 1000, + }, + dd: { + value: 'd', + pad: 2, + }, + h: { // hours + value: 60 * 60 * 1000, + }, + hh: { // padded hours + value: 'h', + pad: 2, + }, + m: { // minutes + value: 60 * 1000, + }, + mm: { // padded minutes + value: 'm', + pad: 2, + }, + s: { // seconds + value: 1000, + }, + ss: { // padded seconds + value: 's', + pad: 2, + }, + sss: { // milliseconds + value: 1, + }, + ssss: { // padded milliseconds + value: 'sss', + pad: 4, + }, + }; + + function _parseFormat(string) { + // @inspiration AngularJS date filter + var parts = []; + var format = string ? string.toString() : ''; + + while (format) { + var match = DURATION_FORMATS_SPLIT.exec(format); + + if (match) { + parts = parts.concat(match.slice(1)); + + format = parts.pop(); + } else { + parts.push(format); + + format = null; + } + } + + return parts; + } + + function _formatDuration(timestamp, format) { + var text = ''; + var values = { }; + + format.filter(function(format) { // filter only value parts of format + return DURATION_FORMATS.hasOwnProperty(format); + }).map(function(format) { // get formats with values only + var config = DURATION_FORMATS[format]; + + if (config.hasOwnProperty('pad')) { + return config.value; + } else { + return format; + } + }).filter(function(format, index, arr) { // remove duplicates + return (arr.indexOf(format) === index); + }).map(function(format) { // get format configurations with values + return angular.extend({ + name: format, + }, DURATION_FORMATS[format]); + }).sort(function(a, b) { // sort formats descending by value + return b.value - a.value; + }).forEach(function(format) { // create values for format parts + var value = values[format.name] = Math.floor(timestamp / format.value); + + timestamp = timestamp - (value * format.value); + }); + + format.forEach(function(part) { + var format = DURATION_FORMATS[part]; + + if (format) { + var value = values[format.value]; + + text += (format.hasOwnProperty('pad') ? _padNumber(value, Math.max(format.pad, value.toString().length)) : values[part]); + } else { + text += part.replace(/(^'|'$)/g, '').replace(/''/g, '\''); + } + }); + + return text; + } + + function _padNumber(number, len) { + return ((new Array(len + 1)).join('0') + number).slice(-len); + } + + return function(value, format) { + var parsedValue = parseFloat(value, 10); + var parsedFormat = _parseFormat(format); + + if (isNaN(parsedValue) || (parsedFormat.length === 0)) { + return value; + } else { + return _formatDuration(parsedValue*1000, parsedFormat); + } + }; + }); + +Array.prototype.append = function(array){ + this.push.apply(this, array) +}; +function copyObjectTo(from, to) { + for (var p in from) { + if (from.hasOwnProperty(p)) { + to[p] = from[p]; + } + } +} +function getUrl(uri) { + uri = uri.hasOwnProperty("resource") ? uri.resource : uri; + uri = uri.hasOwnProperty("uri") ? uri.uri : uri; + if (uri.indexOf("тема:") == 0) { + return "t/" + encodeURIComponent(uri.replace("тема:", "")) + } + uri = uri.replace("статья:", "a/"); + uri = uri.replace("категория:параграф:", ""); + uri = uri.replace("категория:", "c/"); + uri = uri.replace("ии:термин:", ""); + uri = uri.replace("ии:пункт:", ""); + uri = uri.replace("ии:пункты:", ""); + uri = uri.replace("видео:youtube:", "v/"); + uri = uri.replace("документ:google:", "document/"); + uri = uri.replace("изображение:", "picture/"); + uri = uri.replace("запись:", "r/"); + return uri; +} + +function isItemNumber(s) { + return s.match("^\\d+\\.\\d+$"); +} +function isItemRange(s) { + return s.match("^\\d+\\.\\d+-\\d+\\.\\d+$"); +} +function getSelectedText() { + if (window.getSelection) { + return window.getSelection().toString(); + } else if (document.selection) { + return document.selection.createRange().text; + } + return null; +} +function getSelectionText() { + var text = ""; + if (window.getSelection) { + text = window.getSelection().toString(); + } else if (document.selection && document.selection.type != "Control") { + text = document.selection.createRange().text; + } + return text; +} +function groupByDate(data, field) { + var grouped = {}; + angular.forEach(data, function (v) { + var d = new Date(v[field]); + var diff = Date.now() - d; + var header; + if (diff < 24*60*60000) { + header = "За последние сутки" + } else if (diff < 7*24*60*60000) { + header = "За последнюю неделю" + } else if (diff < 30*7*24*60*60000) { + header = "За последний месяц" + } else { + header = "Больше чем месяц назад" + } + if (!grouped[header]) grouped[header] = []; + grouped[header].push(v) + }); + return grouped +} \ No newline at end of file diff --git a/src/main/resources/static/js/controllers/common.js b/src/main/resources/static/js/controllers/common.js new file mode 100644 index 00000000..c1639307 --- /dev/null +++ b/src/main/resources/static/js/controllers/common.js @@ -0,0 +1,562 @@ +function KnowledgeBaseController($scope, $state, $api, $q, entityService, auth) { + $scope.auth = function () { + auth.authenticate().then(function (user) { + $scope.user = user; + }); + }; + $scope.goToCabinet = $state.goToCabinet; + + $api.topic.last(3).then(function (topics) { + $scope.topics = topics; + }); + $api.resource.video.last(0, 3).then(function (videos) { + $scope.videos = videos; + }); + $api.record.last(0, 3).then(function (records) { + $scope.records = records; + }); + $api.document.last(0, 3).then(function (docs) { + $scope.docs = docs; + }); + $api.picture.last(0, 3).then(function (images) { + $scope.images = images; + }) +} + +function RecordController($scope, $stateParams, $api, messager, modal, audioPlayer) { + $scope.recordLoading = true; + $scope.last = []; + $scope.nameFilter = $stateParams.code; + + load(); + + $scope.rename = function (record) { + modal.prompt("Переименование ответа", record.name, "Переименовать").then(function (name) { + $api.record.rename(record.code, name).then(load) + }) + }; + $scope.playOrPause = audioPlayer.playOrPause; + + $scope.getMore = function () { + load(true); + }; + + function load(next) { + $scope.recordLoading = true; + $scope.singleMode = false; + if (!next) { + $scope.last = []; + $scope.lastNoMore = false; + } + $api.record.get(next ? Math.ceil($scope.last.length / 10) : 0, $scope.nameFilter, $scope.yearFilter, $scope.kindFilter).then(function (records) { + $scope.recordLoading = false; + if (!records.length && next) { + $scope.lastNoMore = true; + return + } + $scope.last.append(records); + $scope.singleMode = records.length == 1; + $scope.record = $scope.singleMode ? records[0] : null; + document.title = $scope.singleMode ? records[0].name : "Аудио ответы" + }, function(response){ + $scope.recordLoading = false; + messager.error("Ошибка загрузки ответа"); + }) + } + + $scope.update = function () { + load() + }; + +} + +function DocumentController($scope, $stateParams, $api, messager, $state, modal) { + load(); + $scope.rename = function (doc) { + modal.prompt("Переименование", doc.name, "Переименовать").then(function (name) { + $api.document.rename(doc.uri,name).then(load); + }) + }; + + $scope.getMore = function () { + load(true); + }; + + function load(next) { + $scope.docLoading = true; + $scope.singleMode = false; + if (!next) { + $scope.last = []; + $scope.lastNoMore = false; + } + if ($stateParams.id) { + $scope.docLoading = true; + $api.document.get($stateParams.id).then(function(doc){ + $scope.docLoading = false; + if (doc.id) { + $scope.doc = doc; + document.title = doc.name; + } else { + $scope.showUrlInput = true; + } + }, function(response){ + $scope.docLoading = false; + messager.error("Ошибка загрузки документа"); + }); + } else { + $scope.showUrlInput = true; + $api.document.last(next ? Math.ceil($scope.last.length / 10) : 0).then(function (list) { + $scope.docLoading = false; + if (!list.length && next) { + $scope.lastNoMore = true; + return + } + $scope.last.append(list); + $scope.singleMode = list.length == 1; + $scope.document = $scope.singleMode ? list[0] : null; + document.title = $scope.singleMode ? list[0].name : "Статьи" + }) + } + } + + + $scope.add = function(){ + $scope.docLoading = true; + $api.document.add($scope.url).then(function(doc){ + $state.goToDoc(doc); + }); + }; + $scope.last = []; + $scope.update = function () { + load() + }; +} +function ImageController($scope, $stateParams, $api, messager, $state, modal, $timeout, statistic) { + document.title = "Иллюстрации и схемы. Загрузка..."; + load(); + $scope.rename = function (img) { + modal.prompt("Переименование ответа", img.name, "Переименовать").then(function (name) { + $api.picture.rename(img.uri, name).then(load); + }) + }; + + $scope.getMore = function () { + load(true); + }; + + function load(next) { + $scope.imgLoading = true; + $scope.imgSearching = false; + $scope.singleMode = false; + $scope.commentIsEmpty = true; + if (!next) { + $scope.last = []; + $scope.lastNoMore = false; + } + if ($stateParams.id) { + $scope.imgLoading = true; + $api.picture.get($stateParams.id).then(function(img){ + $scope.imgLoading = false; + + if(img.comment)$scope.commentIsEmpty = false; + + if (img.id) { + $scope.img = img; + } else { + $scope.showUrlInput = true; + } + document.title = img.name + }, function(response){ + $scope.imgLoading = false; + messager.error("Ошибка загрузки изображения"); + }); + } else { + $scope.showUrlInput = true; + $api.picture.last(next ? Math.ceil($scope.last.length / 10) : 0).then(function (list) { + $scope.imgLoading = false; + $scope.imgSearching = false; + if (!list.length && next) { + $scope.lastNoMore = true; + return + } + $scope.last.append(list); + $scope.singleMode = list.length == 1; + $scope.picture = $scope.singleMode ? list[0] : null; + document.title = $scope.singleMode ? list[0].name : "Иллюстрации и схемы" + }) + } + } + + $scope.add = function(){ + $scope.imgLoading = true; + $api.picture.add($scope.url).then(function(img){ + $state.goToImg(img); + }); + }; + $scope.searchImage = function(){ + $scope.last = []; + $scope.imgLoading = false; + $scope.imgSearching = true; + $scope.imgSearchLoading = true; + $scope.searchMode = true; + $api.picture.search($scope.searchImg).then(function(list){ + $scope.last.append(list); + $scope.lastNoMore = true; + $scope.imgSearchLoading = false; + statistic.registerImageSearch($scope.searchImg); + }); + }; + $scope.addComment = function (img) { + $scope.showAddImgComment = false; + $scope.commentIsEmpty = !img.comment; + $api.picture.updateComment(img.uri, img.comment).then(load); + }; + + $scope.updateComment = function (img) { + modal.prompt("Редактирование комментария", img.comment, "Изменить").then(function (comment) { + $api.picture.updateComment(img.uri, comment).then(load); + }) + + }; + $scope.last = []; + + $scope.update = function () { + load() + }; + + var searchChangeTimer; + $scope.searchChange = function () { + if (searchChangeTimer) $timeout.cancel(searchChangeTimer); + if (!$scope.searchImg) { + load(); + $scope.imgSearching = false; + } else if ($scope.searchImg.length > 2) { + searchChangeTimer = $timeout($scope.searchImage, 500); + } + } +} + +function TopicController($scope, $stateParams, $api, $state, modal, $topicPrompt, messager, $timeout, ngAudio, $rootScope, $termPrompt) { + $scope.name = $stateParams.name; + document.title = $scope.name; + + if (isItemNumber($scope.name)) { + $state.redirectToItem($scope.name); + return; + } + if (isItemRange($scope.name)) { + $state.redirectTo($scope.name); + return; + } + + function load() { + $scope.loading = true; + $api.topic.get($scope.name, true).then(function(topic){ + copyObjectTo(topic, $scope); + document.title = $scope.name; + }, function () { + $state.goToHome(); + })['finally'](function () { + $scope.loading = false; + }); + } + $timeout(load); + $scope.unlink = function (linkedTopic) { + $api.topic.unlink($scope.name, linkedTopic).then(load); + }; + $scope.addParent = function () { + $topicPrompt.prompt().then(function (topic) { + $api.topic.addChild(topic, $scope.name).then(load) + }); + }; + $scope.addChild = function () { + $topicPrompt.prompt().then(function (topic) { + $api.topic.addChild($scope.name, topic).then(load) + }); + }; + $scope.addVideoResource = function () { + $state.goToVideo("") + }; + $scope.addDocResource = function () { + $state.goToDoc("") + }; + $scope.merge = function () { + $topicPrompt.prompt().then(function (topic) { + modal.confirm("Подтвердите объединение тем", "Текущая тема \""+$scope.name+"\" будет удалена из системы, а всё что с ней связанно будет перенесено в выбранную тему (\""+topic+"\"). Подтвержаете объединение?", "Объединить") + .then(function () { + $api.topic.merge($scope.name, topic).then(function () { + $state.goToTopic(topic); + messager.ok("Объединение выполнено") + }) + }) + }) + }; + $scope.linkToTerm = function () { + $termPrompt.prompt($scope.name).then(function (term) { + $api.topic.addFor("ии:термин:"+term, $scope.name).then(function () { + messager.ok("Связь с термином удалась") + }) + }) + }; + $scope.play = function(record) { + if ($scope.currentPlayed) $scope.currentPlayed.played = false; + var volume = 0.5; + if ($rootScope.audio) { + volume = $rootScope.audio.volume; + $rootScope.audio.stop(); + } + $rootScope.audio = ngAudio.load(record.resource.audio_url); + $rootScope.audio.volume = volume; + $rootScope.audio.play(); + record.played = true; + $scope.currentPlayed = record; + }; + $rootScope.$watch('audio.paused', function () { + if ($scope.currentPlayed) $scope.currentPlayed.played = !$rootScope.audio.paused; + }); + $scope.search = function (term) { + $state.goToTerm(term); + } +} +function CategoryController($scope, $stateParams, $api, $state) { + + $scope.name = $stateParams.name; + document.title = $scope.name; + $scope.loading = true; + + $api.category.get($scope.name).then(function(category){ + $scope.loading = false; + copyObjectTo(category, $scope); + document.title = $scope.name; + }); +} +function HomeController($scope, $state, auth) { + $scope.search = function(query) { + if (query) { + $state.goToTerm(query); + } + }; + $scope.auth = function () { + auth.authenticate().then(function (user) { + $scope.user = user; + }); + }; + $scope.goToCabinet = $state.goToCabinet; +} +function ItemController($scope, $stateParams, $state, $api, modal) { + $scope.number = $stateParams.number.trim(); + if (!$scope.number) { + $state.goToHome(); + return + } + if ($scope.number.indexOf("6.") == 0) { + modal.message("", "6 том пока официально не опубликован, по этому его текста нет в системе"); + $state.goToHome(); + return + } + document.title = $scope.number; + + $scope.content = "Загрузка..."; + $api.item.get($scope.number).then(function (item) { + copyObjectTo(item, $scope); + }); + $scope.goPrev = function() { + $state.go($scope.previous); + }; + $scope.goNext = function() { + $state.go($scope.next); + } +} +function ItemRangeController($scope, $stateParams, $api) { + + $scope.from = $stateParams.from; + $scope.to = $stateParams.to; + document.title = "Абзацы " + $scope.from+" - "+$scope.to; + $scope.loading = true; + + $api.item.getRange($scope.from, $scope.to) + .then(function(items){ + $scope.loading = false; + $scope.items = items; + }); +} +function ParagraphController($scope, $stateParams, $api, $state) { + + $scope.number = $stateParams.number; + document.title = "§"+$scope.number; + $scope.loading = true; + $scope.goNext = function() { + $state.go($scope.next); + }; + + $api.category.get($scope.number) + .then(function(paragrapg){ + $scope.loading = false; + copyObjectTo(paragrapg, $scope); + }); +} +function TaggerController($scope, $stateParams, $api) { + $scope.$root.hideLoop = true; + $scope.getTags = function(){ + $scope.loading = true; + $scope.terms = []; + $api.term.getTermsInText($scope.text).then(function(terms){ + $scope.terms = terms; + $scope.loading = false; + }); + }; +} +function TopicTreeController($scope, $stateParams, $api) { + $scope.root = {name: "Классификаторы"}; + load($scope.root); + + function load(obj) { + obj.loading = true; + obj.loaded = false; + obj.children = []; + return $api.topic.get(obj.name, false).then(function (topics) { + var wrappers = []; + for(var i in topics.children) { + if (topics.children.hasOwnProperty(i)) + wrappers.push({name: topics.children[i], loading: false, loaded: false, children: []}) + } + obj.loading = false; + obj.loaded = true; + obj.children = wrappers; + }); + } + $scope.load = load; + $scope.expand = function (node) { + if (node.expanded) { + node.expanded = false; + } else { + if (node.loaded) { + node.expanded = true + } else { + load(node).then(function () { + node.expanded = true; + }) + } + } + } +} +function ResourcesController($scope, $stateParams, $state, Video, errorService, $api, $timeout, $pager, modal) { + $scope.topics = []; + $scope.newTopic = {}; + $scope.last = []; + document.title = "Последние видео ответы"; + var pager = $pager.createGroupedByDate($api.resource.video.last, "created_at", 6); + + if ($stateParams.id) { + $scope.videoLoading = true; + $timeout(Video.get({id: $stateParams.id}, function(video){ + $scope.videoLoading = false; + if (video.id) { + $scope.video = video; + document.title = video.title; + } else { + $scope.showUrlInput = true; + } + }, function(response){ + $scope.videoLoading = false; + errorService.resolve("Ошибка добавления видео: " + response.error); + })); + } else { + $scope.showUrlInput = true; + getLast(); + } + + $scope.save = function(){ + $scope.videoLoading = true; + $api.resource.video.add($scope.url).then(function(video){ + $state.goToVideo(video); + }); + }; + $scope.getMore = function () { + getLast(); + }; + + function getLast() { + $scope.lastLoading = true; + pager.loadNext().then(function (data) { + $scope.last = data.grouped; + $scope.lastLoading = false; + $scope.lastNoMore = data.last; + }) + } + + $scope.updateCode = function() { + modal.prompt("Код видео", $scope.video.code, "Указать/Изменить").then(function (code) { + $api.resource.video.updateCode($scope.video.id, code).then(function(){ + $scope.video.code = code; + }); + }) + } +} + +function ArticleController($scope, $stateParams, $state, $api) { + $scope.id = $stateParams.id; + if (!$scope.id) { + $state.goToHome(); + return + } + + $scope.loading = true; + + $api.article.get($scope.id).then(function (a) { + $scope.loading = false; + document.title = a.name; + copyObjectTo(a, $scope); + }); +} + +function CabinetController($scope, $api, $rootScope, auth, modal, $pager) { + document.title = "Личный кабинет"; + var pager = $pager.createGroupedByDate($api.moderation.lastActions, "created_at", 10); + + if (!auth.isAuthenticated()) auth.authenticate().then(onAuthenticated); + else onAuthenticated(); + + function onAuthenticated() { + $scope.user = $rootScope.user; + loadStatus(); + $scope.confirm = function (id) { + $api.moderation.confirm(id).then(loadStatus); + }; + $scope.cancel = function (id) { + modal.confirm("Подтверждение", "Вы уверены что желаете удалить это предложение?", "Удалить").then(function () { + $api.moderation.cancel(id).then(loadStatus); + }); + } + } + var firstAction; + function loadStatus() { + $api.moderation.pendingActions().then(function (pendingActions) { + $scope.pendingActions = pendingActions; + }); + loadNextLastActions(); + } + $scope.updateName = function() { + modal.prompt("Изменение имени", $scope.user.name, "Изменить").then(function (name) { + $api.user.rename(name).then(function () { + $scope.user.name = name; + }); + }); + }; + $scope.hideActions = function () { + $api.user.hideActionsBefore(firstAction.id).then(loadStatus) + }; + + $scope.loadMoreLastActions = loadNextLastActions; + + function loadNextLastActions() { + pager.loadNext().then(function (data) { + if (data.ungroupedList && data.ungroupedList.length) firstAction = data.ungroupedList[0]; + $scope.lastActions = data.grouped; + $scope.hasActions = data.ungroupedList && data.ungroupedList.length; + $scope.hasMoreActions = !data.last; + }); + } +} + + diff --git a/src/main/resources/static/js/controllers/term.js b/src/main/resources/static/js/controllers/term.js new file mode 100644 index 00000000..172eea72 --- /dev/null +++ b/src/main/resources/static/js/controllers/term.js @@ -0,0 +1,196 @@ +function TermController($scope, $stateParams, $api, $state, statistic, $modal) { + var pageCounter = 0, currentQuery; + var query = $scope.query = $stateParams.name; + if (query == "{{url}}") return; // strange bug + if (!query) { + $state.goToHome(); + return + } + if (isItemNumber(query)) { + $state.goToItem(query); + return + } + if (isItemRange(query)) { + var items = query.split("-"); + $state.goToItemRange(items[0], items[1]); + return + } + query = query.replace("+", " ").replace("_", " ").replace("Обсуждение:", "").trim(); + $scope.name = query; + document.title = query; + $scope.state = $state; + + $scope.loading = true; + + $api.term.get(query).then(function (data) { + if (!data) return noTermHandler(); + + $scope.termFound = true; + copyObjectTo(data, $scope); + if (data && !data.description && !data.shortDescription && !data.quotes.length && !data.related.length) { + statistic.registerEmptyTerm(data.name); + } + if (data && !data.description && !data.shortDescription && !data.quotes.length && data.categories.length) { + $scope.showCategories = true; + } + if (data && !data.description && !data.shortDescription && !data.quotes.length && !data.categories.length) { + $scope.search(); + } + + var metaDescription = (data.shortDescription ? data.shortDescription+"\n" : "") + +(data.description ? data.description : ""); + if ($scope.$root) $scope.$root.metaDescription = metaDescription.trim(); + + var keywords = ''; + for(i in data.related) { + keywords += ","+ data.related[i].name; + } + $scope.$root.metaKeywords = keywords; + $scope.showQuotes = data.quotes.length < 3; + $scope.showCategories = data.categories.length < 3; + //$scope.$root.hideLoop = false; + }, noTermHandler) + ['finally'](function () { + $scope.loading = false; + }); + + function noTermHandler() { + $scope.editMode = true; + //$scope.$root.hideLoop = true; + $scope.search(); + } + + $scope.searchCallback = function() { + return $api.search.suggestions($scope.name) + }; + $scope.suggestionSelected = function(suggestion) { + $state.goToTerm(suggestion); + }; + + $scope.loadNextPage = function () { + searchInContent(); + statistic.searchNextPageLoading(pageCounter, query) + }; + $scope.rateUp = function (item) { + if(getSelectionText()) { + var modalInstance = $modal.open({ + templateUrl: 'search-contribute-form.html', + controller: function ($scope, $modalInstance) { + $scope.text = getSelectionText(); + $scope.term = query; + $scope.quote = function() { + var data = { + uri: "ии:пункт:" + item.number, + query: $scope.term + }; + var quote = getSelectionText() || (item.full ? null : item.quote); + if (quote) data.quote = quote; + $api.post("search/rate/+", data).then(function(){ + $modalInstance.close(); + rateComplete(); + }); + }; + $scope.link = function(type) { + if (!$scope.term || !$scope.text) return; + return $api.term.link($scope.term, $scope.text, type).then(function(){ + $modalInstance.close(); + }); + } + } + }); + return; + } + + if (confirm("Вы можете принять участие в полезном наполнении этого сайта. Добавить в список избранных цитат для этого термина?")) { + var data = { + uri: "ии:пункт:" + item.number, + query: $scope.query + }; + var quote = getSelectionText() || (item.full ? null : item.quote); + if (quote) { + data.quote = quote; + } + $api.post("search/rate/+", data).then(rateComplete); + } + }; + $scope.expand = function(quote) { + quote.loadingFull = true; + $api.item.getContent(quote.uri ? quote.uri : quote.number) + .then(function(r){ + quote.full = r; + }) + ['finally'](function () { + quote.loadingFull = false; + }); + }; + + $scope.navigate = function(termName) { + window.open(termName, '_blank'); + //$state.go(entity); + }; + + $scope.search = function(newQuery) { + if (newQuery) { + $state.goToTerm(newQuery); + return; + } + if (currentQuery == query) { + return; + } + currentQuery = query; + pageCounter = 0; + if ($scope.termFound) { + searchInContent(); + } else { + $api.search.term($scope.query).then(function (r) { + $scope.loadingTerms = false; + $scope.terms = r.terms; + $scope.articles = r.articles; + $scope.exactMatchTerm = r.exactMatchTerm; + $scope.categories = r.categories; + $scope.showSCategories = r.categories.length < 3; + if (!r.categories.length) searchInContent(); + }); + $scope.loadingTerms = true; + } + }; + + function searchInContent() { + $api.search.content($scope.query, $scope.startSearchFrom, pageCounter).then(function (r) { + pageCounter++; + for(var i in r.quotes) { + r.quotes[i].uri = "ии:пункт:"+r.quotes[i].number; + } + $scope.foundQuotes = $scope.foundQuotes ? $scope.foundQuotes.concat(r.quotes) : (r.quotes ? r.quotes : []); + if ($scope.foundQuotes.length == 0) { + // no result + $scope.noResult = $scope.foundQuotes.length == 0; + if (ga) ga('send', 'event', 'not-found', $scope.query); + } + $scope.showLoadMore = r.has_more; + if (r.quotes && !r.quotes.length) { + pageCounter = 0; + } + })['finally'](function(){ + $scope.loadingContents = false; + $scope.loadingMore = false; + }); + if (!$scope.foundQuotes || !$scope.foundQuotes.length) { + $scope.loadingContents = true; + } else { + $scope.loadingMore = true; + } + } + $scope.searchInContent = searchInContent; + + function rateComplete() { + alert("Ваш голос учтён, благодарим за помощь! :)") + } + + $scope.advancedSearch = function() { + $scope.foundQuotes = []; + $scope.loadingContents = true; + pageCounter = 0; + searchInContent(); + } +} diff --git a/src/main/resources/static/js/integration.js b/src/main/resources/static/js/integration.js new file mode 100644 index 00000000..05f25bb0 --- /dev/null +++ b/src/main/resources/static/js/integration.js @@ -0,0 +1,80 @@ +var init, maybeLoadJq, url = "http://ii.ayfaar.org/"; + +function jqueryLoaded() { + jQuery.fn.extend({ + getPath: function() { + var pathes = []; + + this.each(function(index, element) { + var path, $node = jQuery(element); + + while ($node.length) { + var realNode = $node.get(0), name = realNode.localName; + if (!name) { break; } + + name = name.toLowerCase(); + var parent = $node.parent(); + var sameTagSiblings = parent.children(name); + + if (sameTagSiblings.length > 1) + { + allSiblings = parent.children(); + var index = allSiblings.index(realNode) +1; + if (index > 0) { + name += ':nth-child(' + index + ')'; + } + } + + path = name + (path ? '>' + path : ''); + $node = parent; + } + + pathes.push(path); + }); + + return pathes.join(','); + } + }); + jQuery(".itemFullText, .ii-terms-container").each(function() { + var container = jQuery(this); + jQuery.post(url + "api/integration?id=" + container.getPath(), container.text(), function (response) { + var terms = response.entryList; + var html = container.html(); + for (var i in terms) { + var key = terms[i].key; + var term = terms[i].value; + var re = new RegExp('(' + $4 + '' + $5; + }); + } + container.html(html); + }) + }) +} + + +init = function() { + jQuery(document).ready(function() { + jqueryLoaded(); + }); +}; + +maybeLoadJq = function() { + var jQ; + if (!(typeof jQuery !== "undefined" && jQuery !== null)) { + jQ = document.createElement('script'); + jQ.type = 'text/javascript'; + jQ.onload = jQ.onreadystatechange = init; + jQ.src = '//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js'; + return document.body.appendChild(jQ); + } else { + return init(); + } +}; + +if (window.addEventListener) { + window.addEventListener('load', maybeLoadJq, false); +} else if (window.attachEvent) { + window.attachEvent('onload', maybeLoadJq); +} diff --git a/src/main/resources/static/lib/angular-ui-router.js b/src/main/resources/static/lib/angular-ui-router.js new file mode 100644 index 00000000..26f76574 --- /dev/null +++ b/src/main/resources/static/lib/angular-ui-router.js @@ -0,0 +1,4539 @@ +/** + * State-based routing for AngularJS + * @version v0.2.18 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = 'ui.router'; +} + +(function (window, angular, undefined) { +/*jshint globalstrict:true*/ +/*global angular:false*/ +'use strict'; + +var isDefined = angular.isDefined, + isFunction = angular.isFunction, + isString = angular.isString, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + toJson = angular.toJson; + +function inherit(parent, extra) { + return extend(new (extend(function() {}, { prototype: parent }))(), extra); +} + +function merge(dst) { + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + if (!dst.hasOwnProperty(key)) dst[key] = value; + }); + } + }); + return dst; +} + +/** + * Finds the common ancestor path between two states. + * + * @param {Object} first The first state. + * @param {Object} second The second state. + * @return {Array} Returns an array of state names in descending order, not including the root. + */ +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +/** + * IE8-safe wrapper for `Object.keys()`. + * + * @param {Object} object A JavaScript object. + * @return {Array} Returns the keys of the object as an array. + */ +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + forEach(object, function(val, key) { + result.push(key); + }); + return result; +} + +/** + * IE8-safe wrapper for `Array.prototype.indexOf()`. + * + * @param {Array} array A JavaScript array. + * @param {*} value A value to search the array for. + * @return {Number} Returns the array index value of `value`, or `-1` if not present. + */ +function indexOf(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +/** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} currentParams The value of the current state parameters ($stateParams). + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i] || !parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (indexOf(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +/** + * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. + * + * @param {Object} a The first object. + * @param {Object} b The second object. + * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, + * it defaults to the list of keys in `a`. + * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + */ +function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ +angular.module('ui.router', ['ui.router.state']); + +angular.module('ui.router.compat', ['ui.router']); + +/** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ +$Resolve.$inject = ['$q', '$injector']; +function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
+   * $resolve.study(invocables)(locals, parent, self)
+   * 
+ * is equivalent to + *
+   * $resolve.resolve(invocables, locals, parent, self)
+   * 
+ * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + var invocableKeys = objectKeys(invocables || {}); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, indexOf(cycle, key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + visited[key] = VISIT_IN_PROGRESS; + + if (isString(value)) { + plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + } else { + var params = $injector.annotate(value); + forEach(params, function (param) { + if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); + }); + plan.push(key, value, params); + } + + cycle.pop(); + visited[key] = VISIT_DONE; + } + forEach(invocables, visit); + invocables = cycle = visited = null; // plan is all that's required + + function isResolve(value) { + return isObject(value) && value.then && value.$$promises; + } + + return function (locals, parent, self) { + if (isResolve(locals) && self === undefined) { + self = parent; parent = locals; locals = null; + } + if (!locals) locals = NO_LOCALS; + else if (!isObject(locals)) { + throw new Error("'locals' must be an object"); + } + if (!parent) parent = NO_PARENT; + else if (!isResolve(parent)) { + throw new Error("'parent' must be a promise returned by $resolve.resolve()"); + } + + // To complete the overall resolution, we have to wait for the parent + // promise and for the promise for each invokable in our plan. + var resolution = $q.defer(), + result = resolution.promise, + promises = result.$$promises = {}, + values = extend({}, locals), + wait = 1 + plan.length/3, + merged = false; + + function done() { + // Merge parent values we haven't got yet and publish our own $$values + if (!--wait) { + if (!merged) merge(values, parent.$$values); + result.$$values = values; + result.$$promises = result.$$promises || true; // keep for isResolve() + delete result.$$inheritedValues; + resolution.resolve(values); + } + } + + function fail(reason) { + result.$$failure = reason; + resolution.reject(reason); + } + + // Short-circuit if parent has already failed + if (isDefined(parent.$$failure)) { + fail(parent.$$failure); + return result; + } + + if (parent.$$inheritedValues) { + merge(values, omit(parent.$$inheritedValues, invocableKeys)); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + extend(promises, parent.$$promises); + if (parent.$$values) { + merged = merge(values, omit(parent.$$values, invocableKeys)); + result.$$inheritedValues = omit(parent.$$values, invocableKeys); + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); + } + parent.then(done, fail); + } + + // Process each invocable in the plan, but ignore any where a local of the same name exists. + for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * for that string. + */ + this.fromUrl = function (url, params) { + if (isFunction(url)) url = url(params); + if (url == null) return null; + else return $http + .get(url, { cache: $templateCache, headers: { Accept: 'text/html' }}) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @param {Function} provider Function to invoke via `$injector.invoke` + * @param {Object} params Parameters for the template. + * @param {Object} locals Locals to pass to `invoke`. Defaults to + * `{ params: params }`. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromProvider = function (provider, params, locals) { + return $injector.invoke(provider, null, locals || { params: params }); + }; +} + +angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); + +var $$UMFP; // reference to $UrlMatcherFactoryProvider + +/** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list + * of search parameters. Multiple search parameter names are separated by '&'. Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace + * syntax, which optionally allows a regular expression for the parameter to be specified: + * + * * `':'` name - colon placeholder + * * `'*'` name - catch-all placeholder + * * `'{' name '}'` - curly placeholder + * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the + * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). For colon + * placeholders or curly placeholders without an explicit regexp, a path parameter matches any + * number of characters other than '/'. For catch-all placeholders the path parameter matches + * any number of characters. + * + * Examples: + * + * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or + * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. + * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. + * * `'/user/{id:[^/]*}'` - Same as the previous example. + * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the + * path into the parameter 'path'. + * * `'/files/*path'` - ditto. + * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined + * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * @param {Object=} parentMatcher Used to concatenate the pattern/config onto + * an existing UrlMatcher + * + * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + * + * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any + * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ +function UrlMatcher(pattern, config, parentMatcher) { + config = extend({ params: {} }, isObject(config) ? config : {}); + + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // '*' name + // ':' name + // '{' name '}' + // '{' name ':' regexp '}' + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) + // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = '^', last = 0, m, + segments = this.segments = [], + parentParams = parentMatcher ? parentMatcher.params : {}, + params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), + paramNames = []; + + function addParameter(id, type, config, location) { + paramNames.push(id); + if (parentParams[id]) return parentParams[id]; + if (!/^\w+([-.]+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = new $$UMFP.Param(id, type, config, location); + return params[id]; + } + + function quoteRegExp(string, pattern, squash, optional) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + switch(squash) { + case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + case true: + result = result.replace(/\/$/, ''); + surroundPattern = ['(?:\/(', ')|\/)?']; + break; + default: surroundPattern = ['(' + squash + "|", ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; + } + + this.source = pattern; + + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + function matchDetails(m, isSearch) { + var id, regexp, segment, type, cfg, arrayMode; + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + + if (regexp) { + type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + } + + return { + id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + }; + } + + var p, param, segment; + while ((m = placeholder.exec(pattern))) { + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) break; // we're into the search part + + param = addParameter(p.id, p.type, p.cfg, "path"); + compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); + segments.push(p.segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf('?'); + + if (i >= 0) { + var search = this.sourceSearch = segment.substring(i); + segment = segment.substring(0, i); + this.sourcePath = pattern.substring(0, last + i); + + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + param = addParameter(p.id, p.type, p.cfg, "search"); + last = placeholder.lastIndex; + // check if ?& + } + } + } else { + this.sourcePath = pattern; + this.sourceSearch = ''; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); + this.prefix = segments[0]; + this.$$paramNames = paramNames; +} + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns a new matcher for a pattern constructed by appending the path part and adding the + * search parameters of the specified pattern to this pattern. The current pattern is not + * modified. This can be understood as creating a pattern for URLs that are relative to (or + * suffixes of) the current pattern. + * + * @example + * The following two matchers are equivalent: + *
+ * new UrlMatcher('/user/{id}?q').concat('/details?date');
+ * new UrlMatcher('/user/{id}/details?q&date');
+ * 
+ * + * @param {string} pattern The pattern to append. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ +UrlMatcher.prototype.concat = function (pattern, config) { + // Because order of search parameters is irrelevant, we can add our own search + // parameters to the end of the new pattern. Parse the new pattern by itself + // and then join the bits together, but it's much easier to do this on a string level. + var defaultConfig = { + caseInsensitive: $$UMFP.caseInsensitive(), + strict: $$UMFP.strictMode(), + squash: $$UMFP.defaultSquashPolicy() + }; + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); +}; + +UrlMatcher.prototype.toString = function () { + return this.source; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Tests the specified path against this matcher, and returns an object containing the captured + * parameter values, or null if the path does not match. The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `searchParams`. This means that search parameters are always treated + * as optional. + * + * @example + *
+ * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
+ *   x: '1', q: 'hello'
+ * });
+ * // returns { id: 'bob', q: 'hello', r: null }
+ * 
+ * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ +UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var paramNames = this.parameters(), nTotal = paramNames.length, + nPath = this.segments.length - 1, + values = {}, i, j, cfg, paramName; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + function decodePathArray(string) { + function reverseString(str) { return str.split("").reverse().join(""); } + function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } + + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); + } + + var param, paramVal; + for (i = 0; i < nPath; i++) { + paramName = paramNames[i]; + param = this.params[paramName]; + paramVal = m[i+1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); + values[paramName] = param.value(paramVal); + } + for (/**/; i < nTotal; i++) { + paramName = paramNames[i]; + values[paramName] = this.params[paramName].value(searchParams[paramName]); + param = this.params[paramName]; + paramVal = searchParams[paramName]; + for (j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); + values[paramName] = param.value(paramVal); + } + + return values; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ +UrlMatcher.prototype.parameters = function (param) { + if (!isDefined(param)) return this.$$paramNames; + return this.params[param] || null; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validates + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ +UrlMatcher.prototype.validates = function (params) { + return this.params.$$validates(params); +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. Null values for path parameters are + * treated as empty strings. + * + * @example + *
+ * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
+ * // returns '/user/bob?q=yes'
+ * 
+ * + * @param {Object} values the values to substitute for the parameters in this pattern. + * @returns {string} the formatted URL (path and optionally search part). + */ +UrlMatcher.prototype.format = function (values) { + values = values || {}; + var segments = this.segments, params = this.parameters(), paramset = this.params; + if (!this.validates(values)) return null; + + var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; + + function encodeDashes(str) { // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); + } + + for (i = 0; i < nTotal; i++) { + var isPathParam = i < nPath; + var name = params[i], param = paramset[name], value = param.value(values[name]); + var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); + var squash = isDefaultValue ? param.squash : false; + var encoded = param.type.encode(value); + + if (isPathParam) { + var nextSegment = segments[i + 1]; + var isFinalPathParam = i + 1 === nPath; + + if (squash === false) { + if (encoded != null) { + if (isArray(encoded)) { + result += map(encoded, encodeDashes).join("-"); + } else { + result += encodeURIComponent(encoded); + } + } + result += nextSegment; + } else if (squash === true) { + var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; + result += nextSegment.match(capture)[1]; + } else if (isString(squash)) { + result += squash + nextSegment; + } + + if (isFinalPathParam && param.squash === true && result.slice(-1) === '/') result = result.slice(0, -1); + } else { + if (encoded == null || (isDefaultValue && squash !== false)) continue; + if (!isArray(encoded)) encoded = [ encoded ]; + if (encoded.length === 0) continue; + encoded = map(encoded, encodeURIComponent).join('&' + name + '='); + result += (search ? '&' : '?') + (name + '=' + encoded); + search = true; + } + } + + return result; +}; + +/** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object which contains the custom type definition. The object's + * properties will override the default methods and/or pattern in `Type`'s public interface. + * @example + *
+ * {
+ *   decode: function(val) { return parseInt(val, 10); },
+ *   encode: function(val) { return val && val.toString(); },
+ *   equals: function(a, b) { return this.is(a) && a === b; },
+ *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
+ *   pattern: /\d+/
+ * }
+ * 
+ * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ +function Type(config) { + extend(this, config); +} + +/** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ +Type.prototype.is = function(val, key) { + return true; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ +Type.prototype.encode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a parameter value (from URL string or transition param) to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ +Type.prototype.decode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ +Type.prototype.equals = function(a, b) { + return a == b; +}; + +Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); +}; + +Type.prototype.pattern = /.*/; + +Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; + +/** Given an encoded string, or a decoded object, returns a decoded object */ +Type.prototype.$normalize = function(val) { + return this.is(val) ? val : this.decode(val); +}; + +/* + * Wraps an existing custom Type as an array of Type, depending on 'mode'. + * e.g.: + * - urlmatcher pattern "/path?{queryParam[]:int}" + * - url: "/path?queryParam=1&queryParam=2 + * - $stateParams.queryParam will be [1, 2] + * if `mode` is "auto", then + * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 + * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] + */ +Type.prototype.$asArray = function(mode, isSearch) { + if (!mode) return this; + if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + + function ArrayType(type, mode) { + function bindTo(type, callbackName) { + return function() { + return type[callbackName].apply(type, arguments); + }; + } + + // Wrap non-array value as array + function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + // Unwrap array value for "auto" mode. Return undefined for empty array. + function arrayUnwrap(val) { + switch(val.length) { + case 0: return undefined; + case 1: return mode === "auto" ? val[0] : val; + default: return val; + } + } + function falsey(val) { return !val; } + + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array + function arrayHandler(callback, allTruthyMode) { + return function handleArray(val) { + if (isArray(val) && val.length === 0) return val; + val = arrayWrap(val); + var result = map(val, callback); + if (allTruthyMode === true) + return filter(result, falsey).length === 0; + return arrayUnwrap(result); + }; + } + + // Wraps type (.equals) functions to operate on each value of an array + function arrayEqualsHandler(callback) { + return function handleArray(val1, val2) { + var left = arrayWrap(val1), right = arrayWrap(val2); + if (left.length !== right.length) return false; + for (var i = 0; i < left.length; i++) { + if (!callback(left[i], right[i])) return false; + } + return true; + }; + } + + this.encode = arrayHandler(bindTo(type, 'encode')); + this.decode = arrayHandler(bindTo(type, 'decode')); + this.is = arrayHandler(bindTo(type, 'is'), true); + this.equals = arrayEqualsHandler(bindTo(type, 'equals')); + this.pattern = type.pattern; + this.$normalize = arrayHandler(bindTo(type, '$normalize')); + this.name = type.name; + this.$arrayMode = mode; + } + + return new ArrayType(this, mode); +}; + + + +/** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ +function $UrlMatcherFactory() { + $$UMFP = this; + + var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; + + // Use tildes to pre-encode slashes. + // If the slashes are simply URLEncoded, the browser can choose to pre-decode them, + // and bidirectional encoding/decoding fails. + // Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character + function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; } + + var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { + "string": { + encode: valToString, + decode: valFromString, + // TODO: in 1.0, make string .is() return false if value is undefined/null by default. + // In 0.2.x, string params are optional by default for backwards compat + is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, + pattern: /[^/]*/ + }, + "int": { + encode: valToString, + decode: function(val) { return parseInt(val, 10); }, + is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, + pattern: /\d+/ + }, + "bool": { + encode: function(val) { return val ? 1 : 0; }, + decode: function(val) { return parseInt(val, 10) !== 0; }, + is: function(val) { return val === true || val === false; }, + pattern: /0|1/ + }, + "date": { + encode: function (val) { + if (!this.is(val)) + return undefined; + return [ val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2) + ].join("-"); + }, + decode: function (val) { + if (this.is(val)) return val; + var match = this.capture.exec(val); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, + equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, + capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ + }, + "json": { + encode: angular.toJson, + decode: angular.fromJson, + is: angular.isObject, + equals: angular.equals, + pattern: /[^/]*/ + }, + "any": { // does not encode/decode + encode: angular.identity, + decode: angular.identity, + equals: angular.equals, + pattern: /.*/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + * @returns {boolean} the current value of caseInsensitive + */ + this.caseInsensitive = function(value) { + if (isDefined(value)) + isCaseInsensitive = value; + return isCaseInsensitive; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. + * @returns {boolean} the current value of strictMode + */ + this.strictMode = function(value) { + if (isDefined(value)) + isStrictMode = value; + return isStrictMode; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Sets the default behavior when generating or matching URLs with default parameter values. + * + * @param {string} value A string that defines the default parameter URL squashing behavior. + * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL + * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the + * parameter is surrounded by slashes, squash (remove) one slash from the URL + * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) + * the parameter value from the URL and replace it with this string. + */ + this.defaultSquashPolicy = function(value) { + if (!isDefined(value)) return defaultSquashPolicy; + if (value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + defaultSquashPolicy = value; + return value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} definition The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * @param {Object|Function} definitionFn (optional) A function that is injected before the app + * runtime starts. The result of this function is merged into the existing `definition`. + * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
+   * var list = ['John', 'Paul', 'George', 'Ringo'];
+   *
+   * $urlMatcherFactoryProvider.type('listItem', {
+   *   encode: function(item) {
+   *     // Represent the list item in the URL using its corresponding index
+   *     return list.indexOf(item);
+   *   },
+   *   decode: function(item) {
+   *     // Look up the list item by index
+   *     return list[parseInt(item, 10)];
+   *   },
+   *   is: function(item) {
+   *     // Ensure the item is valid by checking to see that it appears
+   *     // in the list
+   *     return list.indexOf(item) > -1;
+   *   }
+   * });
+   *
+   * $stateProvider.state('list', {
+   *   url: "/list/{item:listItem}",
+   *   controller: function($scope, $stateParams) {
+   *     console.log($stateParams.item);
+   *   }
+   * });
+   *
+   * // ...
+   *
+   * // Changes URL to '/list/3', logs "Ringo" to the console
+   * $state.go('list', { item: "Ringo" });
+   * 
+ * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
+   * // Defines a custom type that gets a value from a service,
+   * // where each service gets different types of values from
+   * // a backend API:
+   * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
+   *
+   *   // Matches up services to URL parameter names
+   *   var services = {
+   *     user: Users,
+   *     post: Posts
+   *   };
+   *
+   *   return {
+   *     encode: function(object) {
+   *       // Represent the object in the URL using its unique ID
+   *       return object.id;
+   *     },
+   *     decode: function(value, key) {
+   *       // Look up the object by ID, using the parameter
+   *       // name (key) to call the correct service
+   *       return services[key].findById(value);
+   *     },
+   *     is: function(object, key) {
+   *       // Check that object is a valid dbObject
+   *       return angular.isObject(object) && object.id && services[key];
+   *     }
+   *     equals: function(a, b) {
+   *       // Check the equality of decoded objects by comparing
+   *       // their unique IDs
+   *       return a.id === b.id;
+   *     }
+   *   };
+   * });
+   *
+   * // In a config() block, you can then attach URLs with
+   * // type-annotated parameters:
+   * $stateProvider.state('users', {
+   *   url: "/users",
+   *   // ...
+   * }).state('users.item', {
+   *   url: "/{user:dbObject}",
+   *   controller: function($scope, $stateParams) {
+   *     // $stateParams.user will now be an object returned from
+   *     // the Users service
+   *   },
+   *   // ...
+   * });
+   * 
+ */ + this.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) return $types[name]; + if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + + $types[name] = new Type(extend({ name: name }, definition)); + if (definitionFn) { + typeQueue.push({ name: name, def: definitionFn }); + if (!enqueue) flushTypeQueue(); + } + return this; + }; + + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s + function flushTypeQueue() { + while(typeQueue.length) { + var type = typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + angular.extend($types[type.name], injector.invoke(type.def)); + } + } + + // Register default types. Store them in the prototype of $types. + forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + $types = inherit($types, {}); + + /* No need to document $get, since it returns this */ + this.$get = ['$injector', function ($injector) { + injector = $injector; + enqueue = false; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!$types[name]) $types[name] = new Type(type); + }); + return this; + }]; + + this.Param = function Param(id, type, config, location) { + var self = this; + config = unwrapShorthand(config); + type = getType(config, type, location); + var arrayMode = getArrayMode(); + type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) + config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + var isOptional = config.value !== undefined; + var squash = getSquashPolicy(config, isOptional); + var replace = getReplace(config, arrayMode, isOptional, squash); + + function unwrapShorthand(config) { + var keys = isObject(config) ? objectKeys(config) : []; + var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && + indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + if (isShorthand) config = { value: config }; + config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; + return config; + } + + function getType(config, urlType, location) { + if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (urlType) return urlType; + if (!config.type) return (location === "config" ? $types.any : $types.string); + + if (angular.isString(config.type)) + return $types[config.type]; + if (config.type instanceof Type) + return config.type; + return new Type(config.type); + } + + // array config: param name (param[]) overrides default settings. explicit config overrides param name. + function getArrayMode() { + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; + return extend(arrayDefaults, arrayParamNomenclature, config).array; + } + + /** + * returns false, true, or the squash value to indicate the "default parameter url squash policy". + */ + function getSquashPolicy(config, isOptional) { + var squash = config.squash; + if (!isOptional || squash === false) return false; + if (!isDefined(squash) || squash == null) return defaultSquashPolicy; + if (squash === true || isString(squash)) return squash; + throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + } + + function getReplace(config, arrayMode, isOptional, squash) { + var replace, configuredKeys, defaultPolicy = [ + { from: "", to: (isOptional || arrayMode ? undefined : "") }, + { from: null, to: (isOptional || arrayMode ? undefined : "") } + ]; + replace = isArray(config.replace) ? config.replace : []; + if (isString(squash)) + replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function(item) { return item.from; } ); + return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + function $$getDefaultValue() { + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + var defaultValue = injector.invoke(config.$$fn); + if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) + throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); + return defaultValue; + } + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function $replace(value) { + var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + return replacement.length ? replacement[0] : value; + } + value = $replace(value); + return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); + } + + function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } + + extend(this, { + id: id, + type: type, + location: location, + array: arrayMode, + squash: squash, + replace: replace, + isOptional: isOptional, + value: $value, + dynamic: undefined, + config: config, + toString: toString + }); + }; + + function ParamSet(params) { + extend(this, params || {}); + } + + ParamSet.prototype = { + $$new: function() { + return inherit(this, extend(new ParamSet(), { $$parent: this})); + }, + $$keys: function () { + var keys = [], chain = [], parent = this, + ignore = objectKeys(ParamSet.prototype); + while (parent) { chain.push(parent); parent = parent.$$parent; } + chain.reverse(); + forEach(chain, function(paramset) { + forEach(objectKeys(paramset), function(key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + }); + }); + return keys; + }, + $$values: function(paramValues) { + var values = {}, self = this; + forEach(self.$$keys(), function(key) { + values[key] = self[key].value(paramValues && paramValues[key]); + }); + return values; + }, + $$equals: function(paramValues1, paramValues2) { + var equal = true, self = this; + forEach(self.$$keys(), function(key) { + var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + if (!self[key].type.equals(left, right)) equal = false; + }); + return equal; + }, + $$validates: function $$validate(paramValues) { + var keys = this.$$keys(), i, param, rawVal, normalized, encoded; + for (i = 0; i < keys.length; i++) { + param = this[keys[i]]; + rawVal = paramValues[keys[i]]; + if ((rawVal === undefined || rawVal === null) && param.isOptional) + break; // There was no parameter value, but the param is optional + normalized = param.type.$normalize(rawVal); + if (!param.type.is(normalized)) + return false; // The value was not of the correct Type, and could not be decoded to the correct Type + encoded = param.type.encode(normalized); + if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) + return false; // The value was of the correct type, but when encoded, did not match the Type's regexp + } + return true; + }, + $$parent: undefined + }; + + this.ParamSet = ParamSet; +} + +// Register as a provider so it's available to other providers +angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); +angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); + +/** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
+   * var app = angular.module('app', ['ui.router.router']);
+   *
+   * app.config(function ($urlRouterProvider) {
+   *   // Here's an example of how you might allow case insensitive urls
+   *   $urlRouterProvider.rule(function ($injector, $location) {
+   *     var path = $location.path(),
+   *         normalized = path.toLowerCase();
+   *
+   *     if (path !== normalized) {
+   *       return normalized;
+   *     }
+   *   });
+   * });
+   * 
+ * + * @param {function} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
+   * var app = angular.module('app', ['ui.router.router']);
+   *
+   * app.config(function ($urlRouterProvider) {
+   *   // if the path doesn't match any of the urls you configured
+   *   // otherwise will take care of routing the user to the
+   *   // specified url
+   *   $urlRouterProvider.otherwise('/index');
+   *
+   *   // Example of using function rule as param
+   *   $urlRouterProvider.otherwise(function ($injector, $location) {
+   *     return '/a/valid/url';
+   *   });
+   * });
+   * 
+ * + * @param {string|function} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Registers a handler for a given url matching. + * + * If the handler is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. It gets invoked if `$location` + * matches. You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * @example + *
+   * var app = angular.module('app', ['ui.router.router']);
+   *
+   * app.config(function ($urlRouterProvider) {
+   *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
+   *     if ($state.$current.navigable !== state ||
+   *         !equalForKeys($match, $stateParams) {
+   *      $state.transitionTo(state, $match, false);
+   *     }
+   *   });
+   * });
+   * 
+ * + * @param {string|object} what The incoming path that you want to redirect. + * @param {string|function} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ['$match', function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : '' + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
+   * var app = angular.module('app', ['ui.router.router']);
+   *
+   * app.config(function ($urlRouterProvider) {
+   *
+   *   // Prevent $urlRouter from automatically intercepting URL changes;
+   *   // this allows you to configure custom behavior in between
+   *   // location changes and route synchronization:
+   *   $urlRouterProvider.deferIntercept();
+   *
+   * }).run(function ($rootScope, $urlRouter, UserService) {
+   *
+   *   $rootScope.$on('$locationChangeSuccess', function(e) {
+   *     // UserService is an example service for managing user state
+   *     if (UserService.isLoggedIn()) return;
+   *
+   *     // Prevent $urlRouter's default handler from firing
+   *     e.preventDefault();
+   *
+   *     UserService.handleLogin().then(function() {
+   *       // Once the user has logged in, sync the current URL
+   *       // to the router:
+   *       $urlRouter.sync();
+   *     });
+   *   });
+   *
+   *   // Configures $urlRouter's listener *after* your custom listener
+   *   $urlRouter.listen();
+   * });
+   * 
+ * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser', '$sniffer']; + function $get( $location, $rootScope, $injector, $browser, $sniffer) { + + var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; + lastPushedUrl = undefined; + // TODO: Re-implement this in 1.0 for https://github.com/angular-ui/ui-router/issues/1573 + //if (ignoreUpdate) return true; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on('$locationChangeSuccess', update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
+       * angular.module('app', ['ui.router'])
+       *   .run(function($rootScope, $urlRouter) {
+       *     $rootScope.$on('$locationChangeSuccess', function(evt) {
+       *       // Halt state change from even starting
+       *       evt.preventDefault();
+       *       // Perform custom logic
+       *       var meetsRequirement = ...
+       *       // Continue with the update and state transition if logic allows
+       *       if (meetsRequirement) $urlRouter.sync();
+       *     });
+       * });
+       * 
+ */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + var url = urlMatcher.format(params || {}); + + // Handle the special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + $location.url(url); + lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined; + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
+       * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
+       *   person: "bob"
+       * });
+       * // $bob == "/about/bob";
+       * 
+ * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + if (angular.isObject(isHtml5)) { + isHtml5 = isHtml5.enabled; + } + + isHtml5 = isHtml5 && $sniffer.history; + + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + + // Handle special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } +} + +angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); + +/** + * @ngdoc object + * @name ui.router.state.$stateProvider + * + * @requires ui.router.router.$urlRouterProvider + * @requires ui.router.util.$urlMatcherFactoryProvider + * + * @description + * The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely + * on state. + * + * A state corresponds to a "place" in the application in terms of the overall UI and + * navigation. A state describes (via the controller / template / view properties) what + * the UI looks like and does at that place. + * + * States often have things in common, and the primary way of factoring out these + * commonalities in this model is via the state hierarchy, i.e. parent/child states aka + * nested states. + * + * The `$stateProvider` provides interfaces to declare these states for your app. + */ +$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; +function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { + + var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + + // Builds state properties from definition passed to registerState() + var stateBuilder = { + + // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. + // state.children = []; + // if (parent) parent.children.push(state); + parent: function(state) { + if (isDefined(state.parent) && state.parent) return findState(state.parent); + // regex matches any valid composite state name + // would match "contact.list" but not "contacts" + var compositeName = /^(.+)\.[^.]+$/.exec(state.name); + return compositeName ? findState(compositeName[1]) : root; + }, + + // inherit 'data' from parent and override by own values (if any) + data: function(state) { + if (state.parent && state.parent.data) { + state.data = state.self.data = inherit(state.parent.data, state.data); + } + return state.data; + }, + + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: function(state) { + var url = state.url, config = { params: state.params || {} }; + + if (isString(url)) { + if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config); + return (state.parent.navigable || root).url.concat(url, config); + } + + if (!url || $urlMatcherFactory.isMatcher(url)) return url; + throw new Error("Invalid url '" + url + "' in state '" + state + "'"); + }, + + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) + navigable: function(state) { + return state.url ? state : (state.parent ? state.parent.navigable : null); + }, + + // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params + ownParams: function(state) { + var params = state.url && state.url.params || new $$UMFP.ParamSet(); + forEach(state.params || {}, function(config, id) { + if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config"); + }); + return params; + }, + + // Derive parameters for this state and ensure they're a super-set of parent's parameters + params: function(state) { + var ownParams = pick(state.ownParams, state.ownParams.$$keys()); + return state.parent && state.parent.params ? extend(state.parent.params.$$new(), ownParams) : new $$UMFP.ParamSet(); + }, + + // If there is no explicit multi-view configuration, make one up so we don't have + // to handle both cases in the view directive later. Note that having an explicit + // 'views' property will mean the default unnamed view properties are ignored. This + // is also a good time to resolve view names to absolute names, so everything is a + // straight lookup at link time. + views: function(state) { + var views = {}; + + forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { + if (name.indexOf('@') < 0) name += '@' + state.parent.name; + views[name] = view; + }); + return views; + }, + + // Keep a full path from the root down to this state as this is needed for state activation. + path: function(state) { + return state.parent ? state.parent.path.concat(state) : []; // exclude root from path + }, + + // Speed up $state.contains() as it's used a lot + includes: function(state) { + var includes = state.parent ? extend({}, state.parent.includes) : {}; + includes[state.name] = true; + return includes; + }, + + $delegates: {} + }; + + function isRelative(stateName) { + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + } + + function findState(stateOrName, base) { + if (!stateOrName) return undefined; + + var isStr = isString(stateOrName), + name = isStr ? stateOrName : stateOrName.name, + path = isRelative(name); + + if (path) { + if (!base) throw new Error("No reference point given for path '" + name + "'"); + base = findState(base); + + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; + + for (; i < pathLength; i++) { + if (rel[i] === "" && i === 0) { + current = base; + continue; + } + if (rel[i] === "^") { + if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + current = current.parent; + continue; + } + break; + } + rel = rel.slice(i).join("."); + name = current.name + (current.name && rel ? "." : "") + rel; + } + var state = states[name]; + + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + return undefined; + } + + function queueState(parentName, state) { + if (!queue[parentName]) { + queue[parentName] = []; + } + queue[parentName].push(state); + } + + function flushQueuedChildren(parentName) { + var queued = queue[parentName] || []; + while(queued.length) { + registerState(queued.shift()); + } + } + + function registerState(state) { + // Wrap a new object around the state so we can store our private details easily. + state = inherit(state, { + self: state, + resolve: state.resolve || {}, + toString: function() { return this.name; } + }); + + var name = state.name; + if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); + if (states.hasOwnProperty(name)) throw new Error("State '" + name + "' is already defined"); + + // Get parent name + var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) + : (isString(state.parent)) ? state.parent + : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name + : ''; + + // If parent is not registered yet, add state to queue and register later + if (parentName && !states[parentName]) { + return queueState(parentName, state.self); + } + + for (var key in stateBuilder) { + if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + } + states[name] = state; + + // Register the state in the global state list and with $urlRouter if necessary. + if (!state[abstractKey] && state.url) { + $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { inherit: true, location: false }); + } + }]); + } + + // Register any queued children + flushQueuedChildren(name); + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf('*') > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split('.'), + segments = $state.$current.name.split('.'); + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === '*') { + segments[i] = '*'; + } + } + + //match greedy starts + if (globSegments[0] === '**') { + segments = segments.slice(indexOf(segments, globSegments[1])); + segments.unshift('**'); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === '**') { + segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push('**'); + } + + if (globSegments.length != segments.length) { + return false; + } + + return segments.join('') === globSegments.join(''); + } + + + // Implicit root state that is always active + root = registerState({ + name: '', + url: '^', + views: null, + 'abstract': true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl + * based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent's params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * @example + *
+   * // Override the internal 'views' builder with a function that takes the state
+   * // definition, and a reference to the internal function being overridden:
+   * $stateProvider.decorator('views', function (state, parent) {
+   *   var result = {},
+   *       views = parent(state);
+   *
+   *   angular.forEach(views, function (config, name) {
+   *     var autoName = (state.name + '.' + name).replace('.', '/');
+   *     config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
+   *     result[name] = config;
+   *   });
+   *   return result;
+   * });
+   *
+   * $stateProvider.state('home', {
+   *   views: {
+   *     'contact.list': { controller: 'ListController' },
+   *     'contact.item': { controller: 'ItemController' }
+   *   }
+   * });
+   *
+   * // ...
+   *
+   * $state.go('home');
+   * // Auto-populates list and item views with /partials/home/contact/list.html,
+   * // and /partials/home/contact/item.html, respectively.
+   * 
+ * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + this.decorator = decorator; + function decorator(name, func) { + /*jshint validthis: true */ + if (isString(name) && !isDefined(func)) { + return stateBuilder[name]; + } + if (!isFunction(func) || !isString(name)) { + return this; + } + if (stateBuilder[name] && !stateBuilder.$delegates[name]) { + stateBuilder.$delegates[name] = stateBuilder[name]; + } + stateBuilder[name] = func; + return this; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} stateConfig State configuration object. + * @param {string|function=} stateConfig.template + * + * html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
template:
+   *   "

inline template definition

" + + * "
"
+ *
template: function(params) {
+   *       return "

generated template

"; }
+ * + * + * @param {string|function=} stateConfig.templateUrl + * + * + * path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
templateUrl: "home.html"
+ *
templateUrl: function(params) {
+   *     return myTemplates[params.pageId]; }
+ * + * @param {function=} stateConfig.templateProvider + * + * Provider function that returns HTML content string. + *
 templateProvider:
+   *       function(MyTemplateService, params) {
+   *         return MyTemplateService.getTemplate(params.pageId);
+   *       }
+ * + * @param {string|function=} stateConfig.controller + * + * + * Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * Optionally, the ControllerAs may be declared here. + *
controller: "MyRegisteredController"
+ *
controller:
+   *     "MyRegisteredController as fooCtrl"}
+ *
controller: function($scope, MyService) {
+   *     $scope.data = MyService.getData(); }
+ * + * @param {function=} stateConfig.controllerProvider + * + * + * Injectable provider function that returns the actual controller or string. + *
controllerProvider:
+   *   function(MyResolveData) {
+   *     if (MyResolveData.foo)
+   *       return "FooCtrl"
+   *     else if (MyResolveData.bar)
+   *       return "BarCtrl";
+   *     else return function($scope) {
+   *       $scope.baz = "Qux";
+   *     }
+   *   }
+ * + * @param {string=} stateConfig.controllerAs + * + * + * A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + *
controllerAs: "myCtrl"
+ * + * @param {string|object=} stateConfig.parent + * + * Optionally specifies the parent state of this state. + * + *
parent: 'parentState'
+ *
parent: parentState // JS variable
+ * + * @param {object=} stateConfig.resolve + * + * + * An optional map<string, function> of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved before the controller is instantiated. + * If all the promises are resolved successfully, the $stateChangeSuccess event is fired + * and the values of the resolved promises are injected into any controllers that reference them. + * If any of the promises are rejected the $stateChangeError event is fired. + * + * The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + *
resolve: {
+   *     myResolve1:
+   *       function($http, $stateParams) {
+   *         return $http.get("/api/foos/"+stateParams.fooID);
+   *       }
+   *     }
+ * + * @param {string=} stateConfig.url + * + * + * A url fragment with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * (See {@link ui.router.util.type:UrlMatcher UrlMatcher} `UrlMatcher`} for + * more details on acceptable patterns ) + * + * examples: + *
url: "/home"
+   * url: "/users/:userid"
+   * url: "/books/{bookid:[a-zA-Z_-]}"
+   * url: "/books/{categoryid:int}"
+   * url: "/books/{publishername:string}/{categoryid:int}"
+   * url: "/messages?before&after"
+   * url: "/messages?{before:date}&{after:date}"
+   * url: "/messages/:mailboxid?{before:date}&{after:date}"
+   * 
+ * + * @param {object=} stateConfig.views + * + * an optional map<string, object> which defined multiple views, or targets views + * manually/explicitly. + * + * Examples: + * + * Targets three named `ui-view`s in the parent state's template + *
views: {
+   *     header: {
+   *       controller: "headerCtrl",
+   *       templateUrl: "header.html"
+   *     }, body: {
+   *       controller: "bodyCtrl",
+   *       templateUrl: "body.html"
+   *     }, footer: {
+   *       controller: "footCtrl",
+   *       templateUrl: "footer.html"
+   *     }
+   *   }
+ * + * Targets named `ui-view="header"` from grandparent state 'top''s template, and named `ui-view="body" from parent state's template. + *
views: {
+   *     'header@top': {
+   *       controller: "msgHeaderCtrl",
+   *       templateUrl: "msgHeader.html"
+   *     }, 'body': {
+   *       controller: "messagesCtrl",
+   *       templateUrl: "messages.html"
+   *     }
+   *   }
+ * + * @param {boolean=} [stateConfig.abstract=false] + * + * An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + *
abstract: true
+ * + * @param {function=} stateConfig.onEnter + * + * + * Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explicitly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
onEnter: function(MyService, $stateParams) {
+   *     MyService.foo($stateParams.myParam);
+   * }
+ * + * @param {function=} stateConfig.onExit + * + * + * Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explicitly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
onExit: function(MyService, $stateParams) {
+   *     MyService.cleanup($stateParams.myParam);
+   * }
+ * + * @param {boolean=} [stateConfig.reloadOnSearch=true] + * + * + * If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you'd like to modify $location.search() without triggering a reload. + *
reloadOnSearch: false
+ * + * @param {object=} stateConfig.data + * + * + * Arbitrary data object, useful for custom configuration. The parent state's `data` is + * prototypally inherited. In other words, adding a data property to a state adds it to + * the entire subtree via prototypal inheritance. + * + *
data: {
+   *     requiredRole: 'foo'
+   * } 
+ * + * @param {object=} stateConfig.params + * + * + * A map which optionally configures parameters declared in the `url`, or + * defines additional non-url parameters. For each parameter being + * configured, add a configuration object keyed to the name of the parameter. + * + * Each parameter configuration object may contain the following properties: + * + * - ** value ** - {object|function=}: specifies the default value for this + * parameter. This implicitly sets this parameter as optional. + * + * When UI-Router routes to a state and no value is + * specified for this parameter in the URL or transition, the + * default value will be used instead. If `value` is a function, + * it will be injected and invoked, and the return value used. + * + * *Note*: `undefined` is treated as "no default value" while `null` + * is treated as "the default value is `null`". + * + * *Shorthand*: If you only need to configure the default value of the + * parameter, you may use a shorthand syntax. In the **`params`** + * map, instead mapping the param name to a full parameter configuration + * object, simply set map it to the default parameter value, e.g.: + * + *
// define a parameter's default value
+   * params: {
+   *     param1: { value: "defaultValue" }
+   * }
+   * // shorthand default values
+   * params: {
+   *     param1: "defaultValue",
+   *     param2: "param2Default"
+   * }
+ * + * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be + * treated as an array of values. If you specified a Type, the value will be + * treated as an array of the specified Type. Note: query parameter values + * default to a special `"auto"` mode. + * + * For query parameters in `"auto"` mode, if multiple values for a single parameter + * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values + * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if + * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single + * value (e.g.: `{ foo: '1' }`). + * + *
params: {
+   *     param1: { array: true }
+   * }
+ * + * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when + * the current parameter value is the same as the default value. If `squash` is not set, it uses the + * configured default squash policy. + * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`}) + * + * There are three squash settings: + * + * - false: The parameter's default value is not squashed. It is encoded and included in the URL + * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed + * by slashes in the state's `url` declaration, then one of those slashes are omitted. + * This can allow for cleaner looking URLs. + * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice. + * + *
params: {
+   *     param1: {
+   *       value: "defaultId",
+   *       squash: true
+   * } }
+   * // squash "defaultValue" to "~"
+   * params: {
+   *     param1: {
+   *       value: "defaultValue",
+   *       squash: "~"
+   * } }
+   * 
+ * + * + * @example + *
+   * // Some state name examples
+   *
+   * // stateName can be a single top-level name (must be unique).
+   * $stateProvider.state("home", {});
+   *
+   * // Or it can be a nested state name. This state is a child of the
+   * // above "home" state.
+   * $stateProvider.state("home.newest", {});
+   *
+   * // Nest states as deeply as needed.
+   * $stateProvider.state("home.newest.abc.xyz.inception", {});
+   *
+   * // state() returns $stateProvider, so you can chain state declarations.
+   * $stateProvider
+   *   .state("home", {})
+   *   .state("about", {})
+   *   .state("contacts", {});
+   * 
+ * + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you'd like to test against the current active state. + * @property {object} current A reference to the state's config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that'll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you're coming from. + */ + this.$get = $get; + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { + + var TransitionSuperseded = $q.reject(new Error('transition superseded')); + var TransitionPrevented = $q.reject(new Error('transition prevented')); + var TransitionAborted = $q.reject(new Error('transition aborted')); + var TransitionFailed = $q.reject(new Error('transition failed')); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
+       * // somewhere, assume lazy.state has not been defined
+       * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
+       *
+       * // somewhere else
+       * $scope.$on('$stateNotFound',
+       * function(event, unfoundState, fromState, fromParams){
+       *     console.log(unfoundState.to); // "lazy.state"
+       *     console.log(unfoundState.toParams); // {a:1, b:2}
+       *     console.log(unfoundState.options); // {inherit:false} + default options
+       * })
+       * 
+ */ + var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + return TransitionFailed; + } + var retryTransition = $state.transition = $q.when(evt.retry); + + retryTransition.then(function() { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, function() { + return TransitionAborted; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, + * controllers reinstantiated, and events re-fired. + * + * @example + *
+     * var app angular.module('app', ['ui.router']);
+     *
+     * app.controller('ctrl', function ($scope, $state) {
+     *   $scope.reload = function(){
+     *     $state.reload();
+     *   }
+     * });
+     * 
+ * + * `reload()` is just an alias for: + *
+     * $state.transitionTo($state.current, $stateParams, { 
+     *   reload: true, inherit: false, notify: true
+     * });
+     * 
+ * + * @param {string=|object=} state - A state name or a state object, which is the root of the resolves to be re-resolved. + * @example + *
+     * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' 
+     * //and current state is 'contacts.detail.item'
+     * var app angular.module('app', ['ui.router']);
+     *
+     * app.controller('ctrl', function ($scope, $state) {
+     *   $scope.reload = function(){
+     *     //will reload 'contact.detail' and 'contact.detail.item' states
+     *     $state.reload('contact.detail');
+     *   }
+     * });
+     * 
+ * + * `reload()` is just an alias for: + *
+     * $state.transitionTo($state.current, $stateParams, { 
+     *   reload: true, inherit: false, notify: true
+     * });
+     * 
+ + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.reload = function reload(state) { + return $state.transitionTo($state.current, $stateParams, { reload: state || true, inherit: false, notify: true}); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
+     * var app = angular.module('app', ['ui.router']);
+     *
+     * app.controller('ctrl', function ($scope, $state) {
+     *   $scope.changeState = function () {
+     *     $state.go('contact.detail');
+     *   };
+     * });
+     * 
+ * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to a parent state + * - `$state.go('^.sibling')` - will go to a sibling state + * - `$state.go('.child.grandchild')` - will go to grandchild state + * + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * defined parameters. Only parameters specified in the state definition can be overridden, new + * parameters will be ignored. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string|object}, If `true` will force transition even if no state or params + * have changed. It will reload the resolves and views of the current state and parent states. + * If `reload` is a string (or state object), the state object is fetched (by name, or object reference); and \ + * the transition reloads the resolves and views for that matched state, and all its children states. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
Possible rejection values: + * + * - 'transition superseded' - when a newer transition has been started after this one + * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
+     * var app = angular.module('app', ['ui.router']);
+     *
+     * app.controller('ctrl', function ($scope, $state) {
+     *   $scope.changeState = function () {
+     *     $state.transitionTo('contact.detail');
+     *   };
+     * });
+     * 
+ * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string=|object=}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * if String, then will reload the state with the name given in reload, and any children. + * if Object, then a stateObj is expected, will reload the state found in stateObj, and any children. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.transitionTo = function transitionTo(to, toParams, options) { + toParams = toParams || {}; + options = extend({ + location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false + }, options || {}); + + var from = $state.$current, fromParams = $state.params, fromPath = from.path; + var evt, toState = findState(to, options.relative); + + // Store the hash param for later (since it will be stripped out by various methods) + var hash = toParams['#']; + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // Always retry once if the $stateNotFound was not prevented + // (handles either redirect changed or state lazy-definition) + to = redirect.to; + toParams = redirect.toParams; + options = redirect.options; + toState = findState(to, options.relative); + + if (!isDefined(toState)) { + if (!options.relative) throw new Error("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (!toState.params.$$validates(toParams)) return TransitionFailed; + + toParams = toState.params.$$values(toParams); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven't changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } else if (isString(options.reload) || isObject(options.reload)) { + if (isObject(options.reload) && !options.reload.name) { + throw new Error('Invalid reload state object'); + } + + var reloadState = options.reload === true ? fromPath[0] : findState(options.reload); + if (options.reload && !reloadState) { + throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); + } + + while (state && state === fromPath[keep] && state !== reloadState) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // If we're going to the same state and all locals are kept, we've got nothing to do. + // But clear 'transition', as we still want to cancel any other pending transitions. + // TODO: We may not want to bump 'transition' if we're called from a location change + // that we've initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? + if (shouldSkipReload(to, toParams, from, fromParams, locals, options)) { + if (hash) toParams['#'] = hash; + $state.params = toParams; + copy($state.params, $stateParams); + copy(filterByKeys(to.params.$$keys(), $stateParams), to.locals.globals.$stateParams); + if (options.location && to.navigable && to.navigable.url) { + $urlRouter.push(to.navigable.url, toParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + $urlRouter.update(true); + } + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(to.params.$$keys(), toParams || {}); + + // Re-add the saved hash before we start returning things or broadcasting $stateChangeStart + if (hash) toParams['#'] = hash; + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `'transition prevented'` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
+         * $rootScope.$on('$stateChangeStart',
+         * function(event, toState, toParams, fromState, fromParams){
+         *     event.preventDefault();
+         *     // transitionTo() promise will be rejected with
+         *     // a 'transition prevented' error
+         * })
+         * 
+ */ + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams, options).defaultPrevented) { + $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams); + //Don't update and resync url if there's been a new transition started. see issue #2238, #600 + if ($state.transition == null) $urlRouter.update(); + return TransitionPrevented; + } + } + + // Resolve locals for the remaining states, but don't update any global state just + // yet -- if anything fails to resolve the current state needs to remain untouched. + // We also set up an inheritance chain for the locals here. This allows the view directive + // to quickly look up the correct definition for each view in the current state. Even + // though we create the locals object itself outside resolveState(), it is initially + // empty and gets filled asynchronously. We need to keep track of the promise for the + // (fully resolved) current locals, and pass this down the chain. + var resolved = $q.when(locals); + + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals, options); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; + } + + // Enter 'to' states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params + * must match with none missing and no extras. + * + * @example + *
+     * $state.$current.name = 'contacts.details.item';
+     *
+     * // absolute name
+     * $state.is('contact.details.item'); // returns true
+     * $state.is(contactDetailItemStateObject); // returns true
+     *
+     * // relative name (. and ^), typically from a template
+     * // E.g. from the 'contacts.details' template
+     * 
Item
+ *
+ * + * @param {string|object} stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object} - If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it is the state. + */ + $state.is = function is(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) { return undefined; } + if ($state.$current !== state) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you'd like to test for equality. + * + * @example + * Partial and relative names + *
+     * $state.$current.name = 'contacts.details.item';
+     *
+     * // Using partial names
+     * $state.includes("contacts"); // returns true
+     * $state.includes("contacts.details"); // returns true
+     * $state.includes("contacts.details.item"); // returns true
+     * $state.includes("contacts.list"); // returns false
+     * $state.includes("about"); // returns false
+     *
+     * // Using relative names (. and ^), typically from a template
+     * // E.g. from the 'contacts.details' template
+     * 
Item
+ *
+ * + * Basic globbing patterns + *
+     * $state.$current.name = 'contacts.details.item.url';
+     *
+     * $state.includes("*.details.*.*"); // returns true
+     * $state.includes("*.details.**"); // returns true
+     * $state.includes("**.item.**"); // returns true
+     * $state.includes("*.details.item.url"); // returns true
+     * $state.includes("*.details.*.url"); // returns true
+     * $state.includes("*.details.*"); // returns false
+     * $state.includes("item.**"); // returns false
+     * 
+ * + * @param {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object=} - If `stateOrName` is a relative state reference and `options.relative` is set, + * .includes will test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + + var state = findState(stateOrName, options.relative); + if (!isDefined(state)) { return undefined; } + if (!isDefined($state.$current.includes[state.name])) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) : true; + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
+     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
+     * 
+ * + * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. + * @param {object=} params An object of parameter values to fill the state's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || nav.url === undefined || nav.url === null) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys().concat('#'), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|object=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @param {string|object=} context When stateOrName is a relative state reference, the state will be retrieved relative to context. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + var state = findState(stateOrName, context || $state.$current); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); + var locals = { $stateParams: $stateParams }; + + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. + // We're also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + function resolveViews() { + var viewsPromises = []; + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: dst.globals, params: $stateParams, notify: options.notify }) || ''; + }]; + + viewsPromises.push($resolve.resolve(injectables, dst.globals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, dst.globals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + })); + }); + + return $q.all(viewsPromises).then(function(){ + return dst.globals; + }); + } + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(resolveViews).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldSkipReload(to, toParams, from, fromParams, locals, options) { + // Return true if there are no differences in non-search (path/object) params, false if there are differences + function nonSearchParamsEqual(fromAndToState, fromParams, toParams) { + // Identify whether all the parameters that differ between `fromParams` and `toParams` were search params. + function notSearchParam(key) { + return fromAndToState.params[key].location != "search"; + } + var nonQueryParamKeys = fromAndToState.params.$$keys().filter(notSearchParam); + var nonQueryParams = pick.apply({}, [fromAndToState.params].concat(nonQueryParamKeys)); + var nonQueryParamSet = new $$UMFP.ParamSet(nonQueryParams); + return nonQueryParamSet.$$equals(fromParams, toParams); + } + + // If reload was not explicitly requested + // and we're transitioning to the same state we're already in + // and the locals didn't change + // or they changed in a way that doesn't merit reloading + // (reloadOnParams:false, or reloadOnSearch.false and only search params changed) + // Then return true. + if (!options.reload && to === from && + (locals === from.locals || (to.self.reloadOnSearch === false && nonSearchParamsEqual(from, fromParams, toParams)))) { + return true; + } + } +} + +angular.module('ui.router.state') + .factory('$stateParams', function () { return {}; }) + .provider('$state', $StateProvider); + + +$ViewProvider.$inject = []; +function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ['$rootScope', '$templateFactory']; + function $get( $rootScope, $templateFactory) { + return { + // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + return result; + } + }; + } +} + +angular.module('ui.router.state').provider('$view', $ViewProvider); + +/** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ +function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + return $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; +} + +angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); + +var ngMajorVer = angular.version.major; +var ngMinorVer = angular.version.minor; +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} name A view name. The name should be unique amongst the other views in the + * same state. You can have views of the same name that live in different states. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you + * scroll ui-view elements into view when they are populated during a state activation. + * + * @param {string=} noanimation If truthy, the non-animated renderer will be selected (no animations + * will be applied to the ui-view) + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
+ * 
+ * 
+ * + * + *
+ *
+ * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + *
+ * 
+ * $stateProvider.state("home", { + * template: "

HELLO!

" + * }) + *
+ * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} + * config property, by name, in this case an empty name: + *
+ * $stateProvider.state("home", {
+ *   views: {
+ *     "": {
+ *       template: "

HELLO!

" + * } + * } + * }) + *
+ * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + *
+ * 
+ *
+ *
+ * $stateProvider.state("home", {
+ *   views: {
+ *     "main": {
+ *       template: "

HELLO!

" + * } + * } + * }) + *
+ * + * Really though, you'll use views to set up multiple views: + *
+ * 
+ *
+ *
+ *
+ * + *
+ * $stateProvider.state("home", {
+ *   views: {
+ *     "": {
+ *       template: "

HELLO!

" + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
+ * + * Examples for `autoscroll`: + * + *
+ * 
+ * 
+ *
+ * 
+ * 
+ * 
+ * 
+ * 
+ */ +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate']; +function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + + if (!!attrs.noanimation) return statics; + + function animEnabled(element) { + if (ngMajorVer === 1 && ngMinorVer >= 4) return !!$animate.enabled(element); + if (ngMajorVer === 1 && ngMinorVer >= 2) return !!$animate.enabled(); + return (!!$animator); + } + + // ng 1.2+ + if ($animate) { + return { + enter: function(element, target, cb) { + if (!animEnabled(element)) { + statics.enter(element, target, cb); + } else if (angular.version.minor > 2) { + $animate.enter(element, null, target).then(cb); + } else { + $animate.enter(element, null, target, cb); + } + }, + leave: function(element, cb) { + if (!animEnabled(element)) { + statics.leave(element, cb); + } else if (angular.version.minor > 2) { + $animate.leave(element).then(cb); + } else { + $animate.leave(element, cb); + } + } + }; + } + + // ng 1.1.5 + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics; + } + + var directive = { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on('$stateChangeSuccess', function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + var _previousEl = previousEl; + var _currentScope = currentScope; + + if (_currentScope) { + _currentScope._willBeDestroyed = true; + } + + function cleanOld() { + if (_previousEl) { + _previousEl.remove(); + } + + if (_currentScope) { + _currentScope.$destroy(); + } + } + + if (currentEl) { + renderer.leave(currentEl, function() { + cleanOld(); + previousEl = null; + }); + + previousEl = currentEl; + } else { + cleanOld(); + previousEl = null; + } + + currentEl = null; + currentScope = null; + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(scope, attrs, $element, $interpolate), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals || scope._willBeDestroyed) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoading + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {string} viewName Name of the view. + */ + newScope.$emit('$viewContentLoading', name); + + var clone = $transclude(newScope, function(clone) { + renderer.enter(clone, $element, function onUiViewEnter() { + if(currentScope) { + currentScope.$emit('$viewContentAnimationEnded'); + } + + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {string} viewName Name of the view. + */ + currentScope.$emit('$viewContentLoaded', name); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; +} + +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; +function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + name = getUiViewName(scope, attrs, $element, $interpolate), + locals = current && current.locals[name]; + + if (! locals) { + return; + } + + $element.data('$uiView', { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + locals.$element = $element; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + +/** + * Shared ui-view code for both directives: + * Given scope, element, and its attributes, return the view's name + */ +function getUiViewName(scope, attrs, element, $interpolate) { + var name = $interpolate(attrs.uiView || attrs.name || '')(scope); + var inherited = element.inheritedData('$uiView'); + return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); +} + +angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); + +function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + '(' + preparsed[1] + ')'; + parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function stateContext(el) { + var stateData = el.parent().inheritedData('$uiView'); + + if (stateData && stateData.state && stateData.state.name) { + return stateData.state; + } +} + +function getTypeInfo(el) { + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var isSvg = Object.prototype.toString.call(el.prop('href')) === '[object SVGAnimatedString]'; + var isForm = el[0].nodeName === "FORM"; + + return { + attr: isForm ? "action" : (isSvg ? 'xlink:href' : 'href'), + isAnchor: el.prop("tagName").toUpperCase() === "A", + clickable: !isForm + }; +} + +function clickHook(el, $state, $timeout, type, current) { + return function(e) { + var button = e.which || e.button, target = current(); + + if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function() { + $state.go(target.state, target.params, target.options); + }); + e.preventDefault(); + + // if the state has no URL, ignore one preventDefault from the directive. + var ignorePreventDefaultCount = type.isAnchor && !target.href ? 1: 0; + + e.preventDefault = function() { + if (ignorePreventDefaultCount-- <= 0) $timeout.cancel(transition); + }; + } + }; +} + +function defaultOpts(el, $state) { + return { relative: stateContext(el) || $state.$current, inherit: true }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * following template: + *
+ * Home | About | Next page
+ *
+ * 
+ * 
+ * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
+ * Home | About | Next page
+ *
+ * 
    + *
  • + * Joe + *
  • + *
  • + * Alice + *
  • + *
  • + * Bob + *
  • + *
+ * + * Home + *
+ * + * @param {string} ui-sref 'stateName' can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ +$StateRefDirective.$inject = ['$state', '$timeout']; +function $StateRefDirective($state, $timeout) { + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var def = { state: ref.state, href: null, params: null }; + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + + def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {}); + + var update = function(val) { + if (val) def.params = angular.copy(val); + def.href = $state.href(ref.state, def.params, def.options); + + if (active) active.$$addStateInfo(ref.state, def.params); + if (def.href !== null) attrs.$set(type.attr, def.href); + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(val) { if (val !== def.params) update(val); }, true); + def.params = angular.copy(scope.$eval(ref.paramExpr)); + } + update(); + + if (!type.clickable) return; + element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; })); + } + }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-state + * + * @requires ui.router.state.uiSref + * + * @restrict A + * + * @description + * Much like ui-sref, but will accept named $scope properties to evaluate for a state definition, + * params and override options. + * + * @param {string} ui-state 'stateName' can be any valid absolute or relative state + * @param {Object} ui-state-params params to pass to {@link ui.router.state.$state#href $state.href()} + * @param {Object} ui-state-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ +$StateRefDynamicDirective.$inject = ['$state', '$timeout']; +function $StateRefDynamicDirective($state, $timeout) { + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null]; + var watch = '[' + group.map(function(val) { return val || 'null'; }).join(', ') + ']'; + var def = { state: null, params: null, options: null, href: null }; + + function runStateRefLink (group) { + def.state = group[0]; def.params = group[1]; def.options = group[2]; + def.href = $state.href(def.state, def.params, def.options); + + if (active) active.$$addStateInfo(def.state, def.params); + if (def.href) attrs.$set(type.attr, def.href); + } + + scope.$watch(watch, runStateRefLink, true); + runStateRefLink(scope.$eval(watch)); + + if (!type.clickable) return; + element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; })); + } + }; +} + + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive's state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state's menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref's target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it's children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
+ * 
+ * 
+ * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + *
+ * 
+ * 
+ * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * Multiple classes may be specified in a space-separated format: + *
+ * 
    + *
  • + * link + *
  • + *
+ *
+ * + * It is also possible to pass ui-sref-active an expression that evaluates + * to an object hash, whose keys represent active class names and whose + * values represent the respective state names/globs. + * ui-sref-active will match if the current active state **includes** any of + * the specified state names/globs, even the abstract ones. + * + * @Example + * Given the following template, with "admin" being an abstract state: + *
+ * 
+ * Roles + *
+ *
+ * + * When the current state is "admin.roles" the "active" class will be applied + * to both the
and elements. It is important to note that the state + * names/globs passed to ui-sref-active shadow the state provided by ui-sref. + */ + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ +$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; +function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) { + var states = [], activeClasses = {}, activeEqClass, uiSrefActive; + + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope); + + try { + uiSrefActive = $scope.$eval($attrs.uiSrefActive); + } catch (e) { + // Do nothing. uiSrefActive is not a valid expression. + // Fall back to using $interpolate below + } + uiSrefActive = uiSrefActive || $interpolate($attrs.uiSrefActive || '', false)($scope); + if (isObject(uiSrefActive)) { + forEach(uiSrefActive, function(stateOrName, activeClass) { + if (isString(stateOrName)) { + var ref = parseStateRef(stateOrName, $state.current.name); + addState(ref.state, $scope.$eval(ref.paramExpr), activeClass); + } + }); + } + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$addStateInfo = function (newState, newParams) { + // we already got an explicit state provided by ui-sref-active, so we + // shadow the one that comes from ui-sref + if (isObject(uiSrefActive) && states.length > 0) { + return; + } + addState(newState, newParams, uiSrefActive); + update(); + }; + + $scope.$on('$stateChangeSuccess', update); + + function addState(stateName, stateParams, activeClass) { + var state = $state.get(stateName, stateContext($element)); + var stateHash = createStateHash(stateName, stateParams); + + states.push({ + state: state || { name: stateName }, + params: stateParams, + hash: stateHash + }); + + activeClasses[stateHash] = activeClass; + } + + /** + * @param {string} state + * @param {Object|string} [params] + * @return {string} + */ + function createStateHash(state, params) { + if (!isString(state)) { + throw new Error('state should be a string'); + } + if (isObject(params)) { + return state + toJson(params); + } + params = $scope.$eval(params); + if (isObject(params)) { + return state + toJson(params); + } + return state; + } + + // Update route state + function update() { + for (var i = 0; i < states.length; i++) { + if (anyMatch(states[i].state, states[i].params)) { + addClass($element, activeClasses[states[i].hash]); + } else { + removeClass($element, activeClasses[states[i].hash]); + } + + if (exactMatch(states[i].state, states[i].params)) { + addClass($element, activeEqClass); + } else { + removeClass($element, activeEqClass); + } + } + } + + function addClass(el, className) { $timeout(function () { el.addClass(className); }); } + function removeClass(el, className) { el.removeClass(className); } + function anyMatch(state, params) { return $state.includes(state.name, params); } + function exactMatch(state, params) { return $state.is(state.name, params); } + + update(); + }] + }; +} + +angular.module('ui.router.state') + .directive('uiSref', $StateRefDirective) + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEq', $StateRefActiveDirective) + .directive('uiState', $StateRefDynamicDirective); + +/** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ +$IsStateFilter.$inject = ['$state']; +function $IsStateFilter($state) { + var isFilter = function (state, params) { + return $state.is(state, params); + }; + isFilter.$stateful = true; + return isFilter; +} + +/** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $state.includes('fullOrPartialStateName')}. + */ +$IncludedByStateFilter.$inject = ['$state']; +function $IncludedByStateFilter($state) { + var includesFilter = function (state, params, options) { + return $state.includes(state, params, options); + }; + includesFilter.$stateful = true; + return includesFilter; +} + +angular.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); +})(window, window.angular); \ No newline at end of file diff --git a/src/main/resources/static/lib/angular-ui-router.min.js b/src/main/resources/static/lib/angular-ui-router.min.js new file mode 100644 index 00000000..f1b0e351 --- /dev/null +++ b/src/main/resources/static/lib/angular-ui-router.min.js @@ -0,0 +1,8 @@ +/** + * State-based routing for AngularJS + * @version v0.2.18 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return R(new(R(function(){},{prototype:a})),b)}function e(a){return Q(arguments,function(b){b!==a&&Q(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var b=[];return Q(a,function(a,c){b.push(c)}),b}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l]&&i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return R({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(s[c]=d,N(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);Q(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return O(a)&&a.then&&a.$$promises}if(!O(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return Q(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!L(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;Q(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!O(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=l;var n=a.defer(),r=n.promise,s=r.$$promises={},t=R({},d),u=1+q.length/3,v=!1;if(L(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,m(f.$$inheritedValues,p)),R(s,f.$$promises),f.$$values?(v=e(t,m(f.$$values,p)),r.$$inheritedValues=m(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=m(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function q(a,b,c){this.fromConfig=function(a,b,c){return L(a.template)?this.fromString(a.template,b):L(a.templateUrl)?this.fromUrl(a.templateUrl,b):L(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return M(a)?a(b):a},this.fromUrl=function(c,d){return M(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function r(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+([-.]+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new U.Param(b,c,d,e),p[b]}function g(a,b,c,d){var e=["",""],f=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return f;switch(c){case!1:e=["(",")"+(d?"?":"")];break;case!0:f=f.replace(/\/$/,""),e=["(?:/(",")|/)?"];break;default:e=["("+c+"|",")?"]}return f+e[0]+b+e[1]}function h(e,f){var g,h,i,j,k;return g=e[2]||e[3],k=b.params[g],i=a.substring(m,e.index),h=f?e[4]:e[4]||("*"==e[1]?".*":null),h&&(j=U.type(h)||d(U.type("string"),{pattern:new RegExp(h,b.caseInsensitive?"i":c)})),{id:g,regexp:h,segment:i,type:j,cfg:k}}b=R({params:{}},O(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new U.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash,s.isOptional),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function s(a){R(this,a)}function t(){function a(a){return null!=a?a.toString().replace(/~/g,"~~").replace(/\//g,"~2F"):a}function e(a){return null!=a?a.toString().replace(/~2F/g,"/").replace(/~~/g,"~"):a}function f(){return{strict:p,caseInsensitive:m}}function i(a){return M(a)||P(a)&&M(a[a.length-1])}function j(){for(;w.length;){var a=w.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(u[a.name],l.invoke(a.def))}}function k(a){R(this,a||{})}U=this;var l,m=!1,p=!0,q=!1,u={},v=!0,w=[],x={string:{encode:a,decode:e,is:function(a){return null==a||!L(a)||"string"==typeof a},pattern:/[^\/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return L(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^\/]*/},any:{encode:b.identity,decode:b.identity,equals:b.equals,pattern:/.*/}};t.$$getDefaultValue=function(a){if(!i(a.value))return a.value;if(!l)throw new Error("Injectable functions cannot be called at configuration time");return l.invoke(a.value)},this.caseInsensitive=function(a){return L(a)&&(m=a),m},this.strictMode=function(a){return L(a)&&(p=a),p},this.defaultSquashPolicy=function(a){if(!L(a))return q;if(a!==!0&&a!==!1&&!N(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return q=a,a},this.compile=function(a,b){return new r(a,R(f(),b))},this.isMatcher=function(a){if(!O(a))return!1;var b=!0;return Q(r.prototype,function(c,d){M(c)&&(b=b&&L(a[d])&&M(a[d]))}),b},this.type=function(a,b,c){if(!L(b))return u[a];if(u.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return u[a]=new s(R({name:a},b)),c&&(w.push({name:a,def:c}),v||j()),this},Q(x,function(a,b){u[b]=new s(R({name:b},a))}),u=d(u,{}),this.$get=["$injector",function(a){return l=a,v=!1,j(),Q(x,function(a,b){u[b]||(u[b]=new s(a))}),this}],this.Param=function(a,d,e,f){function j(a){var b=O(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=i(a.value)?a.value:function(){return a.value},a}function k(c,d,e){if(c.type&&d)throw new Error("Param '"+a+"' has two type configurations.");return d?d:c.type?b.isString(c.type)?u[c.type]:c.type instanceof s?c.type:new s(c.type):"config"===e?u.any:u.string}function m(){var b={array:"search"===f?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return R(b,c,e).array}function p(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!L(c)||null==c)return q;if(c===!0||N(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function r(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=P(a.replace)?a.replace:[],N(e)&&f.push({from:e,to:c}),g=o(f,function(a){return a.from}),n(i,function(a){return-1===h(g,a.from)}).concat(f)}function t(){if(!l)throw new Error("Injectable functions cannot be called at configuration time");var a=l.invoke(e.$$fn);if(null!==a&&a!==c&&!x.type.is(a))throw new Error("Default value ("+a+") for parameter '"+x.id+"' is not an instance of Type ("+x.type.name+")");return a}function v(a){function b(a){return function(b){return b.from===a}}function c(a){var c=o(n(x.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),L(a)?x.type.$normalize(a):t()}function w(){return"{Param:"+a+" "+d+" squash: '"+A+"' optional: "+z+"}"}var x=this;e=j(e),d=k(e,d,f);var y=m();d=y?d.$asArray(y,"search"===f):d,"string"!==d.name||y||"path"!==f||e.value!==c||(e.value="");var z=e.value!==c,A=p(e,z),B=r(e,y,z,A);R(this,{id:a,type:d,location:f,array:y,squash:A,replace:B,isOptional:z,value:v,dynamic:c,config:e,toString:w})},k.prototype={$$new:function(){return d(this,R(new k,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(k.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),Q(b,function(b){Q(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return Q(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return Q(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var d,e,f,g,h,i=this.$$keys();for(d=0;de;e++)if(b(j[e]))return;k&&b(k)}}function o(){return i=i||e.$on("$locationChangeSuccess",n)}var p,q=g.baseHref(),r=d.url();return l||o(),{sync:function(){n()},listen:function(){return o()},update:function(a){return a?void(r=d.url()):void(d.url()!==r&&(d.url(r),d.replace()))},push:function(a,b,e){var f=a.format(b||{});null!==f&&b&&b["#"]&&(f+="#"+b["#"]),d.url(f),p=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled),g=g&&h.history;var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),null!==i&&e&&e["#"]&&(i+="#"+e["#"]),i=m(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!M(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(N(a)){var b=a;a=function(){return b}}else if(!M(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=N(b);if(N(a)&&(a=d.compile(a)),!h&&!M(b)&&!P(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),R(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:N(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),R(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser","$sniffer"]}function v(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function m(a,b){if(!a)return c;var d=N(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=m(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=z[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function n(a,b){A[a]||(A[a]=[]),A[a].push(b)}function p(a){for(var b=A[a]||[];b.length;)q(b.shift())}function q(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!N(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(z.hasOwnProperty(c))throw new Error("State '"+c+"' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):N(b.parent)?b.parent:O(b.parent)&&N(b.parent.name)?b.parent.name:"";if(e&&!z[e])return n(e,b.self);for(var f in C)M(C[f])&&(b[f]=C[f](b,C.$delegates[f]));return z[c]=b,!b[B]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){y.$current.navigable==b&&j(a,c)||y.transitionTo(b,a,{inherit:!0,location:!1})}]),p(c),b}function r(a){return a.indexOf("*")>-1}function s(a){for(var b=a.split("."),c=y.$current.name.split("."),d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return"**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length?!1:c.join("")===b.join("")}function t(a,b){return N(a)&&!L(b)?C[a]:M(b)&&N(a)?(C[a]&&!C.$delegates[a]&&(C.$delegates[a]=C[a]),C[a]=b,this):this}function u(a,b){return O(a)?b=a:b.name=a,q(b),this}function v(a,e,f,h,l,n,p,q,t){function u(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),D;if(!g.retry)return null;if(f.$retry)return p.update(),E;var h=y.transition=e.when(g.retry);return h.then(function(){return h!==y.transition?A:(b.options.$retry=!0,y.transitionTo(b.to,b.toParams,b.options))},function(){return D}),p.update(),h}function v(a,c,d,g,i,j){function m(){var c=[];return Q(a.views,function(d,e){var g=d.resolve&&d.resolve!==a.resolve?d.resolve:{};g.$template=[function(){return f.load(e,{view:d,locals:i.globals,params:n,notify:j.notify})||""}],c.push(l.resolve(g,i.globals,i.resolve,a).then(function(c){if(M(d.controllerProvider)||P(d.controllerProvider)){var f=b.extend({},g,i.globals);c.$$controller=h.invoke(d.controllerProvider,null,f)}else c.$$controller=d.controller;c.$$state=a,c.$$controllerAs=d.controllerAs,i[e]=c}))}),e.all(c).then(function(){return i.globals})}var n=d?c:k(a.params.$$keys(),c),o={$stateParams:n};i.resolve=l.resolve(a.resolve,o,i.resolve,a);var p=[i.resolve.then(function(a){i.globals=a})];return g&&p.push(g),e.all(p).then(m).then(function(a){return i})}var A=e.reject(new Error("transition superseded")),C=e.reject(new Error("transition prevented")),D=e.reject(new Error("transition aborted")),E=e.reject(new Error("transition failed"));return x.locals={resolve:null,globals:{$stateParams:{}}},y={params:{},current:x.self,$current:x,transition:null},y.reload=function(a){return y.transitionTo(y.current,n,{reload:a||!0,inherit:!1,notify:!0})},y.go=function(a,b,c){return y.transitionTo(a,b,R({inherit:!0,relative:y.$current},c))},y.transitionTo=function(b,c,f){c=c||{},f=R({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=y.$current,l=y.params,o=j.path,q=m(b,f.relative),r=c["#"];if(!L(q)){var s={to:b,toParams:c,options:f},t=u(s,j.self,l,f);if(t)return t;if(b=s.to,c=s.toParams,f=s.options,q=m(b,f.relative),!L(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[B])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(n,c||{},y.$current,q)),!q.params.$$validates(c))return E;c=q.params.$$values(c),b=q;var z=b.path,D=0,F=z[D],G=x.locals,H=[];if(f.reload){if(N(f.reload)||O(f.reload)){if(O(f.reload)&&!f.reload.name)throw new Error("Invalid reload state object");var I=f.reload===!0?o[0]:m(f.reload);if(f.reload&&!I)throw new Error("No such reload state '"+(N(f.reload)?f.reload:f.reload.name)+"'");for(;F&&F===o[D]&&F!==I;)G=H[D]=F.locals,D++,F=z[D]}}else for(;F&&F===o[D]&&F.ownParams.$$equals(c,l);)G=H[D]=F.locals,D++,F=z[D];if(w(b,c,j,l,G,f))return r&&(c["#"]=r),y.params=c,S(y.params,n),S(k(b.params.$$keys(),n),b.locals.globals.$stateParams),f.location&&b.navigable&&b.navigable.url&&(p.push(b.navigable.url,c,{$$avoidResync:!0,replace:"replace"===f.location}),p.update(!0)),y.transition=null,e.when(y.current);if(c=k(b.params.$$keys(),c||{}),r&&(c["#"]=r),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,l,f).defaultPrevented)return a.$broadcast("$stateChangeCancel",b.self,c,j.self,l),null==y.transition&&p.update(),C;for(var J=e.when(G),K=D;K=D;d--)g=o[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d=4?!!j.enabled(a):1===V&&W>=2?!!j.enabled():!!i}var e={enter:function(a,b,c){b.after(a),c()},leave:function(a,b){a.remove(),b()}};if(a.noanimation)return e;if(j)return{enter:function(a,c,f){d(a)?b.version.minor>2?j.enter(a,null,c).then(f):j.enter(a,null,c,f):e.enter(a,c,f)},leave:function(a,c){d(a)?b.version.minor>2?j.leave(a).then(c):j.leave(a,c):e.leave(a,c)}};if(i){var f=i&&i(c,a);return{enter:function(a,b,c){f.enter(a,null,b),c()},leave:function(a,b){f.leave(a),b()}}}return e}var h=f(),i=h("$animator"),j=h("$animate"),k={restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(c,f,h){return function(c,f,i){function j(){function a(){b&&b.remove(),c&&c.$destroy()}var b=l,c=n;c&&(c._willBeDestroyed=!0),m?(r.leave(m,function(){a(),l=null}),l=m):(a(),l=null),m=null,n=null}function k(g){var k,l=A(c,i,f,e),s=l&&a.$current&&a.$current.locals[l];if((g||s!==o)&&!c._willBeDestroyed){k=c.$new(),o=a.$current.locals[l],k.$emit("$viewContentLoading",l);var t=h(k,function(a){r.enter(a,f,function(){n&&n.$emit("$viewContentAnimationEnded"),(b.isDefined(q)&&!q||c.$eval(q))&&d(a)}),j()});m=t,n=k,n.$emit("$viewContentLoaded",l),n.$eval(p)}}var l,m,n,o,p=i.onload||"",q=i.autoscroll,r=g(i,c);c.$on("$stateChangeSuccess",function(){k(!1)}),k(!0)}}};return k}function z(a,b,c,d){return{restrict:"ECA",priority:-400,compile:function(e){var f=e.html();return function(e,g,h){var i=c.$current,j=A(e,h,g,d),k=i&&i.locals[j];if(k){g.data("$uiView",{name:j,state:k.$$state}),g.html(k.$template?k.$template:f);var l=a(g.contents());if(k.$$controller){k.$scope=e,k.$element=g;var m=b(k.$$controller,k);k.$$controllerAs&&(e[k.$$controllerAs]=m),g.data("$ngControllerController",m),g.children().data("$ngControllerController",m)}l(e)}}}}}function A(a,b,c,d){var e=d(b.uiView||b.name||"")(a),f=c.inheritedData("$uiView");return e.indexOf("@")>=0?e:e+"@"+(f?f.state.name:"")}function B(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function C(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function D(a){var b="[object SVGAnimatedString]"===Object.prototype.toString.call(a.prop("href")),c="FORM"===a[0].nodeName;return{attr:c?"action":b?"xlink:href":"href",isAnchor:"A"===a.prop("tagName").toUpperCase(),clickable:!c}}function E(a,b,c,d,e){return function(f){var g=f.which||f.button,h=e();if(!(g>1||f.ctrlKey||f.metaKey||f.shiftKey||a.attr("target"))){var i=c(function(){b.go(h.state,h.params,h.options)});f.preventDefault();var j=d.isAnchor&&!h.href?1:0;f.preventDefault=function(){j--<=0&&c.cancel(i)}}}}function F(a,b){return{relative:C(a)||b.$current,inherit:!0}}function G(a,c){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(d,e,f,g){var h=B(f.uiSref,a.current.name),i={state:h.state,href:null,params:null},j=D(e),k=g[1]||g[0];i.options=R(F(e,a),f.uiSrefOpts?d.$eval(f.uiSrefOpts):{});var l=function(c){c&&(i.params=b.copy(c)),i.href=a.href(h.state,i.params,i.options),k&&k.$$addStateInfo(h.state,i.params),null!==i.href&&f.$set(j.attr,i.href)};h.paramExpr&&(d.$watch(h.paramExpr,function(a){a!==i.params&&l(a)},!0),i.params=b.copy(d.$eval(h.paramExpr))),l(),j.clickable&&e.bind("click",E(e,a,c,j,function(){return i}))}}}function H(a,b){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(c,d,e,f){function g(b){l.state=b[0],l.params=b[1],l.options=b[2],l.href=a.href(l.state,l.params,l.options),i&&i.$$addStateInfo(l.state,l.params),l.href&&e.$set(h.attr,l.href)}var h=D(d),i=f[1]||f[0],j=[e.uiState,e.uiStateParams||null,e.uiStateOpts||null],k="["+j.map(function(a){return a||"null"}).join(", ")+"]",l={state:null,params:null,options:null,href:null};c.$watch(k,g,!0),g(c.$eval(k)),h.clickable&&d.bind("click",E(d,a,b,h,function(){return l}))}}}function I(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs","$timeout",function(b,d,e,f){function g(b,c,e){var f=a.get(b,C(d)),g=h(b,c);p.push({state:f||{name:b},params:c,hash:g}),q[g]=e}function h(a,c){if(!N(a))throw new Error("state should be a string");return O(c)?a+T(c):(c=b.$eval(c),O(c)?a+T(c):a)}function i(){for(var a=0;a0||(g(a,b,o),i())},b.$on("$stateChangeSuccess",i),i()}]}}function J(a){var b=function(b,c){return a.is(b,c)};return b.$stateful=!0,b}function K(a){var b=function(b,c,d){return a.includes(b,c,d)};return b.$stateful=!0,b}var L=b.isDefined,M=b.isFunction,N=b.isString,O=b.isObject,P=b.isArray,Q=b.forEach,R=b.extend,S=b.copy,T=b.toJson;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),p.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",p),q.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",q);var U;r.prototype.concat=function(a,b){var c={caseInsensitive:U.caseInsensitive(),strict:U.strictMode(),squash:U.defaultSquashPolicy()};return new r(this.sourcePath+a+this.sourceSearch,R(c,b),this)},r.prototype.toString=function(){return this.source},r.prototype.exec=function(a,b){function c(a){function b(a){return a.split("").reverse().join("")}function c(a){return a.replace(/\\-/g,"-")}var d=b(a).split(/-(?!\\)/),e=o(d,b);return o(e,c).reverse()}var d=this.regexp.exec(a);if(!d)return null;b=b||{};var e,f,g,h=this.parameters(),i=h.length,j=this.segments.length-1,k={};if(j!==d.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");var l,m;for(e=0;j>e;e++){for(g=h[e],l=this.params[g],m=d[e+1],f=0;fe;e++){for(g=h[e],k[g]=this.params[g].value(b[g]),l=this.params[g],m=b[g],f=0;ff;f++){var k=h>f,l=d[f],m=e[l],n=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),n),q=p?m.squash:!1,r=m.type.encode(n);if(k){var s=c[f+1],t=f+1===h;if(q===!1)null!=r&&(j+=P(r)?o(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var u=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(u)[1]}else N(q)&&(j+=q+s);t&&m.squash===!0&&"/"===j.slice(-1)&&(j=j.slice(0,-1))}else{if(null==r||p&&q!==!1)continue;if(P(r)||(r=[r]),0===r.length)continue;r=o(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},s.prototype.is=function(a,b){return!0},s.prototype.encode=function(a,b){return a},s.prototype.decode=function(a,b){return a},s.prototype.equals=function(a,b){return a==b},s.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},s.prototype.pattern=/.*/,s.prototype.toString=function(){return"{Type:"+this.name+"}"},s.prototype.$normalize=function(a){return this.is(a)?a:this.decode(a)},s.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return P(a)?a:L(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){if(P(c)&&0===c.length)return c;c=e(c);var d=o(c,a);return b===!0?0===n(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g= audioObject.duration) { + completeListeners.forEach(function(listener){ + listener(audioObject); + }) + } + + if ($looping && audioObject.currentTime >= audioObject.duration) { + if ($looping !== true) { + $looping--; + audioObject.loop--; + // if (!$looping) return; + } + audioObject.setCurrentTime(0); + audioObject.play(); + + } + } + + if (!$isMuting && !ngAudioGlobals.isMuting) { + audioObject.volume = audio.volume; + } + + audioObject.audio = audio; + } + + $setWatch(); + } + }; +}]) +.service('ngAudio', ['NgAudioObject', 'ngAudioGlobals', function(NgAudioObject, ngAudioGlobals) { + this.play = function(id) { + + var audio = new NgAudioObject(id); + audio.play(); + return audio; + }; + + this.load = function(id) { + return new NgAudioObject(id); + }; + + this.mute = function() { + ngAudioGlobals.muting = true; + }; + + this.unmute = function() { + ngAudioGlobals.muting = false; + }; + + this.toggleMute = function() { + ngAudioGlobals.muting = !ngAudioGlobals.muting; + }; + + this.setUnlock = function(unlock) { + ngAudioGlobals.unlock = unlock; + }; +}]) +.filter("trackTime", function(){ + /* Conveniently takes a number and returns the track time */ + + return function(input){ + + var totalSec = 0; + + // String manipulation + var inputString = input ? input.toString() : ""; + for (var i = 0; i < inputString.length; i++){ + var dotIndex = inputString.indexOf("."); + totalSec = parseInt(inputString.slice(0, dotIndex)); + } + + var output = ""; + var hours = 0; + var minutes = 0; + var seconds = 0; + + if (totalSec > 3599) { + + hours = Math.floor(totalSec / 3600); + minutes = Math.floor((totalSec - (hours * 3600)) / 60); + seconds = (totalSec - ((minutes * 60) + (hours * 3600))); + + if (hours.toString().length == 1) { + hours = "0" + (Math.floor(totalSec / 3600)).toString(); + } + + if (minutes.toString().length == 1) { + minutes = "0" + (Math.floor((totalSec - (hours * 3600)) / 60)).toString(); + } + + if (seconds.toString().length == 1) { + seconds = "0" + (totalSec - ((minutes * 60) + (hours * 3600))).toString(); + } + + output = hours + ":" + minutes + ":" + seconds; + + } else if (totalSec > 59) { + + minutes = Math.floor(totalSec / 60); + seconds = totalSec - (minutes * 60); + + if (minutes.toString().length == 1) { + minutes = "0" + (Math.floor(totalSec / 60)).toString(); + } + + if (seconds.toString().length == 1) { + seconds = "0" + (totalSec - (minutes * 60)).toString(); + } + + output = minutes + ":" + seconds; + + } else { + + seconds = totalSec; + + if (seconds.toString().length == 1) { + seconds = "0" + (totalSec).toString(); + } + + output = totalSec + "s"; + + } + + /*if (Number.isNaN(output)){ + debugger; + }*/ + + return output; + } +}); diff --git a/src/main/resources/static/lib/angular.audio.min.js b/src/main/resources/static/lib/angular.audio.min.js new file mode 100644 index 00000000..0992416a --- /dev/null +++ b/src/main/resources/static/lib/angular.audio.min.js @@ -0,0 +1 @@ +"use strict";angular.module("ngAudio",[]).directive("ngAudio",["$compile","$q","ngAudio",function(a,b,c){return{restrict:"AEC",scope:{volume:"=",start:"=",currentTime:"=",loop:"=",clickPlay:"=",disablePreload:"="},controller:function(a,b,d,e){function g(){f=c.load(b.ngAudio),a.$audio=f,f.unbind()}var f;a.disablePreload||g(),d.on("click",function(){a.clickPlay!==!1&&(a.disablePreload&&g(),f.audio.play(),f.volume=a.volume||f.volume,f.loop=a.loop,f.currentTime=a.start||0,e(function(){f.play()},5))})}}}]).directive("ngAudioHover",["$compile","$q","ngAudio",function(a,b,c){return{restrict:"AEC",controller:function(a,b,d,e){var f=c.load(b.ngAudioHover);d.on("mouseover rollover hover",function(){f.audio.play(),f.volume=b.volumeHover||f.volume,f.loop=b.loop,f.currentTime=b.startHover||0})}}}]).service("localAudioFindingService",["$q",function(a){this.find=function(b){var c=a.defer(),d=document.getElementById(b);return d?c.resolve(d):c.reject(b),c.promise}}]).service("remoteAudioFindingService",["$q",function(a){this.find=function(b){var c=a.defer(),d=new Audio;return d.addEventListener("error",function(){c.reject()}),d.addEventListener("loadstart",function(){c.resolve(d)}),setTimeout(function(){d.src=b},1),c.promise}}]).service("cleverAudioFindingService",["$q","localAudioFindingService","remoteAudioFindingService",function(a,b,c){this.find=function(d){var e=a.defer();return d=d.replace("|","/"),b.find(d).then(e.resolve,function(){return c.find(d)}).then(e.resolve,e.reject),e.promise}}]).value("ngAudioGlobals",{muting:!1,songmuting:!1,performance:25,unlock:!0}).factory("NgAudioObject",["cleverAudioFindingService","$rootScope","$interval","$timeout","ngAudioGlobals",function(a,b,c,d,e){return function(d){function s(){f=b.$watch(function(){return{volume:q.volume,currentTime:q.currentTime,progress:q.progress,muting:q.muting,loop:q.loop,playbackRate:q.playbackRate}},function(a,b){a.currentTime!==b.currentTime&&q.setCurrentTime(a.currentTime),a.progress!==b.progress&&q.setProgress(a.progress),a.volume!==b.volume&&q.setVolume(a.volume),a.playbackRate!==b.playbackRate&&q.setPlaybackRate(a.playbackRate),m=a.loop,a.muting!==b.muting&&q.setMuting(a.muting)},!0)}function u(){f&&f(),p&&(n||e.isMuting?p.volume=0:p.volume=void 0!==q.volume?q.volume:1,g&&(p.play(),g=!1),i&&(p.pause(),p.currentTime=0,i=!1),h&&(p.pause(),h=!1),j&&(p.playbackRate=k,j=!1),l&&(p.volume=l,l=void 0),o&&(q.currentTime=p.currentTime,q.duration=p.duration,q.remaining=p.duration-p.currentTime,q.progress=p.currentTime/p.duration,q.paused=p.paused,q.src=p.src,q.currentTime>=q.duration&&r.forEach(function(a){a(q)}),m&&q.currentTime>=q.duration&&(m!==!0&&(m--,q.loop--),q.setCurrentTime(0),q.play())),n||e.isMuting||(q.volume=p.volume),q.audio=p),s()}e.unlock&&window.addEventListener("click",function a(){p&&(p.play(),p.pause()),window.removeEventListener("click",a)});var f,l,m,p,g=!1,h=!1,i=!1,j=!1,k=!1,n=!1,o=!0,q=this;this.id=d,this.safeId=d.replace("/","|"),this.loop=0,this.unbind=function(){o=!1},this.play=function(){return g=!0,this};var r=[];this.complete=function(a){r.push(a)},this.pause=function(){h=!0},this.restart=function(){i=!0},this.stop=function(){this.restart()},this.setVolume=function(a){l=a},this.setPlaybackRate=function(a){k=a,j=!0},this.setMuting=function(a){n=a},this.setProgress=function(a){p&&p.duration&&isFinite(a)&&(p.currentTime=p.duration*a)},this.setCurrentTime=function(a){p&&p.duration&&(p.currentTime=a)},a.find(d).then(function(a){p=a,p.addEventListener("canplay",function(){q.canPlay=!0})},function(a){q.error=!0,console.warn(a)});var t=c(u,e.performance);b.$watch(function(){return e.performance},function(){c.cancel(t),t=c(u,e.performance)})}}]).service("ngAudio",["NgAudioObject","ngAudioGlobals",function(a,b){this.play=function(b){var c=new a(b);return c.play(),c},this.load=function(b){return new a(b)},this.mute=function(){b.muting=!0},this.unmute=function(){b.muting=!1},this.toggleMute=function(){b.muting=!b.muting},this.setUnlock=function(a){b.unlock=a}}]).filter("trackTime",function(){return function(a){for(var b=0,c=a?a.toString():"",d=0;d3599?(g=Math.floor(b/3600),h=Math.floor((b-3600*g)/60),i=b-(60*h+3600*g),1==g.toString().length&&(g="0"+Math.floor(b/3600).toString()),1==h.toString().length&&(h="0"+Math.floor((b-3600*g)/60).toString()),1==i.toString().length&&(i="0"+(b-(60*h+3600*g)).toString()),f=g+":"+h+":"+i):b>59?(h=Math.floor(b/60),i=b-60*h,1==h.toString().length&&(h="0"+Math.floor(b/60).toString()),1==i.toString().length&&(i="0"+(b-60*h).toString()),f=h+":"+i):(i=b,1==i.toString().length&&(i="0"+b.toString()),f=b+"s"),f}}); \ No newline at end of file diff --git a/src/main/resources/static/lib/angular/angular-animate.js b/src/main/resources/static/lib/angular/angular-animate.js new file mode 100644 index 00000000..2778fc56 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-animate.js @@ -0,0 +1,4121 @@ +/** + * @license AngularJS v1.5.0 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/* jshint ignore:start */ +var noop = angular.noop; +var copy = angular.copy; +var extend = angular.extend; +var jqLite = angular.element; +var forEach = angular.forEach; +var isArray = angular.isArray; +var isString = angular.isString; +var isObject = angular.isObject; +var isUndefined = angular.isUndefined; +var isDefined = angular.isDefined; +var isFunction = angular.isFunction; +var isElement = angular.isElement; + +var ELEMENT_NODE = 1; +var COMMENT_NODE = 8; + +var ADD_CLASS_SUFFIX = '-add'; +var REMOVE_CLASS_SUFFIX = '-remove'; +var EVENT_CLASS_PREFIX = 'ng-'; +var ACTIVE_CLASS_SUFFIX = '-active'; +var PREPARE_CLASS_SUFFIX = '-prepare'; + +var NG_ANIMATE_CLASSNAME = 'ng-animate'; +var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; + +// Detect proper transitionend/animationend event names. +var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; + +// If unprefixed events are not supported but webkit-prefixed are, use the latter. +// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. +// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` +// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. +// Register both events in case `window.onanimationend` is not supported because of that, +// do the same for `transitionend` as Safari is likely to exhibit similar behavior. +// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit +// therefore there is no reason to test anymore for other vendor prefixes: +// http://caniuse.com/#search=transition +if (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) { + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; +} else { + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; +} + +if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { + CSS_PREFIX = '-webkit-'; + ANIMATION_PROP = 'WebkitAnimation'; + ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; +} else { + ANIMATION_PROP = 'animation'; + ANIMATIONEND_EVENT = 'animationend'; +} + +var DURATION_KEY = 'Duration'; +var PROPERTY_KEY = 'Property'; +var DELAY_KEY = 'Delay'; +var TIMING_KEY = 'TimingFunction'; +var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; +var ANIMATION_PLAYSTATE_KEY = 'PlayState'; +var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; + +var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; +var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; +var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; +var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; + +var isPromiseLike = function(p) { + return p && p.then ? true : false; +}; + +var ngMinErr = angular.$$minErr('ng'); +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + } + return arg; +} + +function mergeClasses(a,b) { + if (!a && !b) return ''; + if (!a) return b; + if (!b) return a; + if (isArray(a)) a = a.join(' '); + if (isArray(b)) b = b.join(' '); + return a + ' ' + b; +} + +function packageStyles(options) { + var styles = {}; + if (options && (options.to || options.from)) { + styles.to = options.to; + styles.from = options.from; + } + return styles; +} + +function pendClasses(classes, fix, isPrefix) { + var className = ''; + classes = isArray(classes) + ? classes + : classes && isString(classes) && classes.length + ? classes.split(/\s+/) + : []; + forEach(classes, function(klass, i) { + if (klass && klass.length > 0) { + className += (i > 0) ? ' ' : ''; + className += isPrefix ? fix + klass + : klass + fix; + } + }); + return className; +} + +function removeFromArray(arr, val) { + var index = arr.indexOf(val); + if (val >= 0) { + arr.splice(index, 1); + } +} + +function stripCommentsFromElement(element) { + if (element instanceof jqLite) { + switch (element.length) { + case 0: + return []; + break; + + case 1: + // there is no point of stripping anything if the element + // is the only element within the jqLite wrapper. + // (it's important that we retain the element instance.) + if (element[0].nodeType === ELEMENT_NODE) { + return element; + } + break; + + default: + return jqLite(extractElementNode(element)); + break; + } + } + + if (element.nodeType === ELEMENT_NODE) { + return jqLite(element); + } +} + +function extractElementNode(element) { + if (!element[0]) return element; + for (var i = 0; i < element.length; i++) { + var elm = element[i]; + if (elm.nodeType == ELEMENT_NODE) { + return elm; + } + } +} + +function $$addClass($$jqLite, element, className) { + forEach(element, function(elm) { + $$jqLite.addClass(elm, className); + }); +} + +function $$removeClass($$jqLite, element, className) { + forEach(element, function(elm) { + $$jqLite.removeClass(elm, className); + }); +} + +function applyAnimationClassesFactory($$jqLite) { + return function(element, options) { + if (options.addClass) { + $$addClass($$jqLite, element, options.addClass); + options.addClass = null; + } + if (options.removeClass) { + $$removeClass($$jqLite, element, options.removeClass); + options.removeClass = null; + } + } +} + +function prepareAnimationOptions(options) { + options = options || {}; + if (!options.$$prepared) { + var domOperation = options.domOperation || noop; + options.domOperation = function() { + options.$$domOperationFired = true; + domOperation(); + domOperation = noop; + }; + options.$$prepared = true; + } + return options; +} + +function applyAnimationStyles(element, options) { + applyAnimationFromStyles(element, options); + applyAnimationToStyles(element, options); +} + +function applyAnimationFromStyles(element, options) { + if (options.from) { + element.css(options.from); + options.from = null; + } +} + +function applyAnimationToStyles(element, options) { + if (options.to) { + element.css(options.to); + options.to = null; + } +} + +function mergeAnimationDetails(element, oldAnimation, newAnimation) { + var target = oldAnimation.options || {}; + var newOptions = newAnimation.options || {}; + + var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || ''); + var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || ''); + var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove); + + if (newOptions.preparationClasses) { + target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses); + delete newOptions.preparationClasses; + } + + // noop is basically when there is no callback; otherwise something has been set + var realDomOperation = target.domOperation !== noop ? target.domOperation : null; + + extend(target, newOptions); + + // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this. + if (realDomOperation) { + target.domOperation = realDomOperation; + } + + if (classes.addClass) { + target.addClass = classes.addClass; + } else { + target.addClass = null; + } + + if (classes.removeClass) { + target.removeClass = classes.removeClass; + } else { + target.removeClass = null; + } + + oldAnimation.addClass = target.addClass; + oldAnimation.removeClass = target.removeClass; + + return target; +} + +function resolveElementClasses(existing, toAdd, toRemove) { + var ADD_CLASS = 1; + var REMOVE_CLASS = -1; + + var flags = {}; + existing = splitClassesToLookup(existing); + + toAdd = splitClassesToLookup(toAdd); + forEach(toAdd, function(value, key) { + flags[key] = ADD_CLASS; + }); + + toRemove = splitClassesToLookup(toRemove); + forEach(toRemove, function(value, key) { + flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS; + }); + + var classes = { + addClass: '', + removeClass: '' + }; + + forEach(flags, function(val, klass) { + var prop, allow; + if (val === ADD_CLASS) { + prop = 'addClass'; + allow = !existing[klass]; + } else if (val === REMOVE_CLASS) { + prop = 'removeClass'; + allow = existing[klass]; + } + if (allow) { + if (classes[prop].length) { + classes[prop] += ' '; + } + classes[prop] += klass; + } + }); + + function splitClassesToLookup(classes) { + if (isString(classes)) { + classes = classes.split(' '); + } + + var obj = {}; + forEach(classes, function(klass) { + // sometimes the split leaves empty string values + // incase extra spaces were applied to the options + if (klass.length) { + obj[klass] = true; + } + }); + return obj; + } + + return classes; +} + +function getDomNode(element) { + return (element instanceof angular.element) ? element[0] : element; +} + +function applyGeneratedPreparationClasses(element, event, options) { + var classes = ''; + if (event) { + classes = pendClasses(event, EVENT_CLASS_PREFIX, true); + } + if (options.addClass) { + classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX)); + } + if (options.removeClass) { + classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX)); + } + if (classes.length) { + options.preparationClasses = classes; + element.addClass(classes); + } +} + +function clearGeneratedClasses(element, options) { + if (options.preparationClasses) { + element.removeClass(options.preparationClasses); + options.preparationClasses = null; + } + if (options.activeClasses) { + element.removeClass(options.activeClasses); + options.activeClasses = null; + } +} + +function blockTransitions(node, duration) { + // we use a negative delay value since it performs blocking + // yet it doesn't kill any existing transitions running on the + // same element which makes this safe for class-based animations + var value = duration ? '-' + duration + 's' : ''; + applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]); + return [TRANSITION_DELAY_PROP, value]; +} + +function blockKeyframeAnimations(node, applyBlock) { + var value = applyBlock ? 'paused' : ''; + var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; + applyInlineStyle(node, [key, value]); + return [key, value]; +} + +function applyInlineStyle(node, styleTuple) { + var prop = styleTuple[0]; + var value = styleTuple[1]; + node.style[prop] = value; +} + +function concatWithSpace(a,b) { + if (!a) return b; + if (!b) return a; + return a + ' ' + b; +} + +var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { + var queue, cancelFn; + + function scheduler(tasks) { + // we make a copy since RAFScheduler mutates the state + // of the passed in array variable and this would be difficult + // to track down on the outside code + queue = queue.concat(tasks); + nextTick(); + } + + queue = scheduler.queue = []; + + /* waitUntilQuiet does two things: + * 1. It will run the FINAL `fn` value only when an uncanceled RAF has passed through + * 2. It will delay the next wave of tasks from running until the quiet `fn` has run. + * + * The motivation here is that animation code can request more time from the scheduler + * before the next wave runs. This allows for certain DOM properties such as classes to + * be resolved in time for the next animation to run. + */ + scheduler.waitUntilQuiet = function(fn) { + if (cancelFn) cancelFn(); + + cancelFn = $$rAF(function() { + cancelFn = null; + fn(); + nextTick(); + }); + }; + + return scheduler; + + function nextTick() { + if (!queue.length) return; + + var items = queue.shift(); + for (var i = 0; i < items.length; i++) { + items[i](); + } + + if (!cancelFn) { + $$rAF(function() { + if (!cancelFn) nextTick(); + }); + } + } +}]; + +/** + * @ngdoc directive + * @name ngAnimateChildren + * @restrict AE + * @element ANY + * + * @description + * + * ngAnimateChildren allows you to specify that children of this element should animate even if any + * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` + * (structural) animation, child elements that also have an active structural animation are not animated. + * + * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). + * + * + * @param {string} ngAnimateChildren If the value is empty, `true` or `on`, + * then child animations are allowed. If the value is `false`, child animations are not allowed. + * + * @example + * + +
+ + +
+
+
+ List of items: +
Item {{item}}
+
+
+
+
+ + + .container.ng-enter, + .container.ng-leave { + transition: all ease 1.5s; + } + + .container.ng-enter, + .container.ng-leave-active { + opacity: 0; + } + + .container.ng-leave, + .container.ng-enter-active { + opacity: 1; + } + + .item { + background: firebrick; + color: #FFF; + margin-bottom: 10px; + } + + .item.ng-enter, + .item.ng-leave { + transition: transform 1.5s ease; + } + + .item.ng-enter { + transform: translateX(50px); + } + + .item.ng-enter-active { + transform: translateX(0); + } + + + angular.module('ngAnimateChildren', ['ngAnimate']) + .controller('mainController', function() { + this.animateChildren = false; + this.enterElement = false; + }); + +
+ */ +var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { + return { + link: function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN_DATA, true); + } else { + // Interpolate and set the value, so that it is available to + // animations that run right after compilation + setData($interpolate(val)(scope)); + attrs.$observe('ngAnimateChildren', setData); + } + + function setData(value) { + value = value === 'on' || value === 'true'; + element.data(NG_ANIMATE_CHILDREN_DATA, value); + } + } + }; +}]; + +var ANIMATE_TIMER_KEY = '$$animateCss'; + +/** + * @ngdoc service + * @name $animateCss + * @kind object + * + * @description + * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes + * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT + * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or + * directives to create more complex animations that can be purely driven using CSS code. + * + * Note that only browsers that support CSS transitions and/or keyframe animations are capable of + * rendering animations triggered via `$animateCss` (bad news for IE9 and lower). + * + * ## Usage + * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that + * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however, + * any automatic control over cancelling animations and/or preventing animations from being run on + * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to + * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger + * the CSS animation. + * + * The example below shows how we can create a folding animation on an element using `ng-if`: + * + * ```html + * + *
+ * This element will go BOOM + *
+ * + * ``` + * + * Now we create the **JavaScript animation** that will trigger the CSS transition: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * ## More Advanced Uses + * + * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks + * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code. + * + * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation, + * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with + * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order + * to provide a working animation that will run in CSS. + * + * The example below showcases a more advanced version of the `.fold-animation` from the example above: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * addClass: 'red large-text pulse-twice', + * easing: 'ease-out', + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * Since we're adding/removing CSS classes then the CSS transition will also pick those up: + * + * ```css + * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code, + * the CSS classes below will be transitioned despite them being defined as regular CSS classes */ + * .red { background:red; } + * .large-text { font-size:20px; } + * + * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */ + * .pulse-twice { + * animation: 0.5s pulse linear 2; + * -webkit-animation: 0.5s pulse linear 2; + * } + * + * @keyframes pulse { + * from { transform: scale(0.5); } + * to { transform: scale(1.5); } + * } + * + * @-webkit-keyframes pulse { + * from { -webkit-transform: scale(0.5); } + * to { -webkit-transform: scale(1.5); } + * } + * ``` + * + * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen. + * + * ## How the Options are handled + * + * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation + * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline + * styles using the `from` and `to` properties. + * + * ```js + * var animator = $animateCss(element, { + * from: { background:'red' }, + * to: { background:'blue' } + * }); + * animator.start(); + * ``` + * + * ```css + * .rotating-animation { + * animation:0.5s rotate linear; + * -webkit-animation:0.5s rotate linear; + * } + * + * @keyframes rotate { + * from { transform: rotate(0deg); } + * to { transform: rotate(360deg); } + * } + * + * @-webkit-keyframes rotate { + * from { -webkit-transform: rotate(0deg); } + * to { -webkit-transform: rotate(360deg); } + * } + * ``` + * + * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is + * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition + * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition + * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied + * and spread across the transition and keyframe animation. + * + * ## What is returned + * + * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually + * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are + * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties: + * + * ```js + * var animator = $animateCss(element, { ... }); + * ``` + * + * Now what do the contents of our `animator` variable look like: + * + * ```js + * { + * // starts the animation + * start: Function, + * + * // ends (aborts) the animation + * end: Function + * } + * ``` + * + * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. + * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been + * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties + * and that changing them will not reconfigure the parameters of the animation. + * + * ### runner.done() vs runner.then() + * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the + * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**. + * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()` + * unless you really need a digest to kick off afterwards. + * + * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss + * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code). + * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works. + * + * @param {DOMElement} element the element that will be animated + * @param {object} options the animation-related options that will be applied during the animation + * + * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied + * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) + * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and + * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. + * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). + * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). + * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). + * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. + * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. + * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation. + * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation. + * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0` + * is provided then the animation will be skipped entirely. + * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is + * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value + * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same + * CSS delay value. + * * `stagger` - A numeric time value representing the delay between successively animated elements + * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) + * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a + * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) + * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occurring on the classes being added and removed.) + * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once + * the animation is closed. This is useful for when the styles are used purely for the sake of + * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation). + * By default this value is set to `false`. + * + * @return {object} an object with start and end methods and details about the animation. + * + * * `start` - The method to start the animation. This will return a `Promise` when called. + * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. + */ +var ONE_SECOND = 1000; +var BASE_TEN = 10; + +var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; +var CLOSING_TIME_BUFFER = 1.5; + +var DETECT_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + transitionProperty: TRANSITION_PROP + PROPERTY_KEY, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP, + animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY +}; + +var DETECT_STAGGER_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP +}; + +function getCssKeyframeDurationStyle(duration) { + return [ANIMATION_DURATION_PROP, duration + 's']; +} + +function getCssDelayStyle(delay, isKeyframeAnimation) { + var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; + return [prop, delay + 's']; +} + +function computeCssStyles($window, element, properties) { + var styles = Object.create(null); + var detectedStyles = $window.getComputedStyle(element) || {}; + forEach(properties, function(formalStyleName, actualStyleName) { + var val = detectedStyles[formalStyleName]; + if (val) { + var c = val.charAt(0); + + // only numerical-based values have a negative sign or digit as the first value + if (c === '-' || c === '+' || c >= 0) { + val = parseMaxTime(val); + } + + // by setting this to null in the event that the delay is not set or is set directly as 0 + // then we can still allow for negative values to be used later on and not mistake this + // value for being greater than any other negative value. + if (val === 0) { + val = null; + } + styles[actualStyleName] = val; + } + }); + + return styles; +} + +function parseMaxTime(str) { + var maxValue = 0; + var values = str.split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; +} + +function truthyTimingValue(val) { + return val === 0 || val != null; +} + +function getCssTransitionDurationStyle(duration, applyOnlyDuration) { + var style = TRANSITION_PROP; + var value = duration + 's'; + if (applyOnlyDuration) { + style += DURATION_KEY; + } else { + value += ' linear all'; + } + return [style, value]; +} + +function createLocalCacheLookup() { + var cache = Object.create(null); + return { + flush: function() { + cache = Object.create(null); + }, + + count: function(key) { + var entry = cache[key]; + return entry ? entry.total : 0; + }, + + get: function(key) { + var entry = cache[key]; + return entry && entry.value; + }, + + put: function(key, value) { + if (!cache[key]) { + cache[key] = { total: 1, value: value }; + } else { + cache[key].total++; + } + } + }; +} + +// we do not reassign an already present style value since +// if we detect the style property value again we may be +// detecting styles that were added via the `from` styles. +// We make use of `isDefined` here since an empty string +// or null value (which is what getPropertyValue will return +// for a non-existing style) will still be marked as a valid +// value for the style (a falsy value implies that the style +// is to be removed at the end of the animation). If we had a simple +// "OR" statement then it would not be enough to catch that. +function registerRestorableStyles(backup, node, properties) { + forEach(properties, function(prop) { + backup[prop] = isDefined(backup[prop]) + ? backup[prop] + : node.style.getPropertyValue(prop); + }); +} + +var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { + var gcsLookup = createLocalCacheLookup(); + var gcsStaggerLookup = createLocalCacheLookup(); + + this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', + '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', + function($window, $$jqLite, $$AnimateRunner, $timeout, + $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + var parentCounter = 0; + function gcsHashFn(node, extraClasses) { + var KEY = "$$ngAnimateParentKey"; + var parentNode = node.parentNode; + var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); + return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; + } + + function computeCachedCssStyles(node, className, cacheKey, properties) { + var timings = gcsLookup.get(cacheKey); + + if (!timings) { + timings = computeCssStyles($window, node, properties); + if (timings.animationIterationCount === 'infinite') { + timings.animationIterationCount = 1; + } + } + + // we keep putting this in multiple times even though the value and the cacheKey are the same + // because we're keeping an internal tally of how many duplicate animations are detected. + gcsLookup.put(cacheKey, timings); + return timings; + } + + function computeCachedCssStaggerStyles(node, className, cacheKey, properties) { + var stagger; + + // if we have one or more existing matches of matching elements + // containing the same parent + CSS styles (which is how cacheKey works) + // then staggering is possible + if (gcsLookup.count(cacheKey) > 0) { + stagger = gcsStaggerLookup.get(cacheKey); + + if (!stagger) { + var staggerClassName = pendClasses(className, '-stagger'); + + $$jqLite.addClass(node, staggerClassName); + + stagger = computeCssStyles($window, node, properties); + + // force the conversion of a null value to zero incase not set + stagger.animationDuration = Math.max(stagger.animationDuration, 0); + stagger.transitionDuration = Math.max(stagger.transitionDuration, 0); + + $$jqLite.removeClass(node, staggerClassName); + + gcsStaggerLookup.put(cacheKey, stagger); + } + } + + return stagger || {}; + } + + var cancelLastRAFRequest; + var rafWaitQueue = []; + function waitUntilQuiet(callback) { + rafWaitQueue.push(callback); + $$rAFScheduler.waitUntilQuiet(function() { + gcsLookup.flush(); + gcsStaggerLookup.flush(); + + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); + + // we use a for loop to ensure that if the queue is changed + // during this looping then it will consider new requests + for (var i = 0; i < rafWaitQueue.length; i++) { + rafWaitQueue[i](pageWidth); + } + rafWaitQueue.length = 0; + }); + } + + function computeTimings(node, className, cacheKey) { + var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); + var aD = timings.animationDelay; + var tD = timings.transitionDelay; + timings.maxDelay = aD && tD + ? Math.max(aD, tD) + : (aD || tD); + timings.maxDuration = Math.max( + timings.animationDuration * timings.animationIterationCount, + timings.transitionDuration); + + return timings; + } + + return function init(element, initialOptions) { + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = prepareAnimationOptions(copy(options)); + } + + var restoreStyles = {}; + var node = getDomNode(element); + if (!node + || !node.parentNode + || !$$animateQueue.enabled()) { + return closeAndReturnNoopAnimator(); + } + + var temporaryStyles = []; + var classes = element.attr('class'); + var styles = packageStyles(options); + var animationClosed; + var animationPaused; + var animationCompleted; + var runner; + var runnerHost; + var maxDelay; + var maxDelayTime; + var maxDuration; + var maxDurationTime; + var startTime; + var events = []; + + if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { + return closeAndReturnNoopAnimator(); + } + + var method = options.event && isArray(options.event) + ? options.event.join(' ') + : options.event; + + var isStructural = method && options.structural; + var structuralClassName = ''; + var addRemoveClassName = ''; + + if (isStructural) { + structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); + } else if (method) { + structuralClassName = method; + } + + if (options.addClass) { + addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); + } + + if (options.removeClass) { + if (addRemoveClassName.length) { + addRemoveClassName += ' '; + } + addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); + } + + // there may be a situation where a structural animation is combined together + // with CSS classes that need to resolve before the animation is computed. + // However this means that there is no explicit CSS code to block the animation + // from happening (by setting 0s none in the class name). If this is the case + // we need to apply the classes before the first rAF so we know to continue if + // there actually is a detected transition or keyframe animation + if (options.applyClassesEarly && addRemoveClassName.length) { + applyAnimationClasses(element, options); + } + + var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); + var fullClassName = classes + ' ' + preparationClasses; + var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); + var hasToStyles = styles.to && Object.keys(styles.to).length > 0; + var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; + + // there is no way we can trigger an animation if no styles and + // no classes are being applied which would then trigger a transition, + // unless there a is raw keyframe value that is applied to the element. + if (!containsKeyframeAnimation + && !hasToStyles + && !preparationClasses) { + return closeAndReturnNoopAnimator(); + } + + var cacheKey, stagger; + if (options.stagger > 0) { + var staggerVal = parseFloat(options.stagger); + stagger = { + transitionDelay: staggerVal, + animationDelay: staggerVal, + transitionDuration: 0, + animationDuration: 0 + }; + } else { + cacheKey = gcsHashFn(node, fullClassName); + stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); + } + + if (!options.$$skipPreparationClasses) { + $$jqLite.addClass(element, preparationClasses); + } + + var applyOnlyDuration; + + if (options.transitionStyle) { + var transitionStyle = [TRANSITION_PROP, options.transitionStyle]; + applyInlineStyle(node, transitionStyle); + temporaryStyles.push(transitionStyle); + } + + if (options.duration >= 0) { + applyOnlyDuration = node.style[TRANSITION_PROP].length > 0; + var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration); + + // we set the duration so that it will be picked up by getComputedStyle later + applyInlineStyle(node, durationStyle); + temporaryStyles.push(durationStyle); + } + + if (options.keyframeStyle) { + var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle]; + applyInlineStyle(node, keyframeStyle); + temporaryStyles.push(keyframeStyle); + } + + var itemIndex = stagger + ? options.staggerIndex >= 0 + ? options.staggerIndex + : gcsLookup.count(cacheKey) + : 0; + + var isFirst = itemIndex === 0; + + // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY + // without causing any combination of transitions to kick in. By adding a negative delay value + // it forces the setup class' transition to end immediately. We later then remove the negative + // transition delay to allow for the transition to naturally do it's thing. The beauty here is + // that if there is no transition defined then nothing will happen and this will also allow + // other transitions to be stacked on top of each other without any chopping them out. + if (isFirst && !options.skipBlocking) { + blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); + } + + var timings = computeTimings(node, fullClassName, cacheKey); + var relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + var flags = {}; + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all'; + flags.applyTransitionDuration = hasToStyles && ( + (flags.hasTransitions && !flags.hasTransitionAll) + || (flags.hasAnimations && !flags.hasTransitions)); + flags.applyAnimationDuration = options.duration && flags.hasAnimations; + flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions); + flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations; + flags.recalculateTimingStyles = addRemoveClassName.length > 0; + + if (flags.applyTransitionDuration || flags.applyAnimationDuration) { + maxDuration = options.duration ? parseFloat(options.duration) : maxDuration; + + if (flags.applyTransitionDuration) { + flags.hasTransitions = true; + timings.transitionDuration = maxDuration; + applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0; + temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration)); + } + + if (flags.applyAnimationDuration) { + flags.hasAnimations = true; + timings.animationDuration = maxDuration; + temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration)); + } + } + + if (maxDuration === 0 && !flags.recalculateTimingStyles) { + return closeAndReturnNoopAnimator(); + } + + if (options.delay != null) { + var delayStyle; + if (typeof options.delay !== "boolean") { + delayStyle = parseFloat(options.delay); + // number in options.delay means we have to recalculate the delay for the closing timeout + maxDelay = Math.max(delayStyle, 0); + } + + if (flags.applyTransitionDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle)); + } + + if (flags.applyAnimationDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle, true)); + } + } + + // we need to recalculate the delay value since we used a pre-emptive negative + // delay value and the delay value is required for the final event checking. This + // property will ensure that this will happen after the RAF phase has passed. + if (options.duration == null && timings.transitionDuration > 0) { + flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + if (!options.skipBlocking) { + flags.blockTransition = timings.transitionDuration > 0; + flags.blockKeyframeAnimation = timings.animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + } + + if (options.from) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); + } + applyAnimationFromStyles(element, options); + } + + if (flags.blockTransition || flags.blockKeyframeAnimation) { + applyBlocking(maxDuration); + } else if (!options.skipBlocking) { + blockTransitions(node, false); + } + + // TODO(matsko): for 1.5 change this code to have an animator object for better debugging + return { + $$willAnimate: true, + end: endFn, + start: function() { + if (animationClosed) return; + + runnerHost = { + end: endFn, + cancel: cancelFn, + resume: null, //this will be set during the start() phase + pause: null + }; + + runner = new $$AnimateRunner(runnerHost); + + waitUntilQuiet(start); + + // we don't have access to pause/resume the animation + // since it hasn't run yet. AnimateRunner will therefore + // set noop functions for resume and pause and they will + // later be overridden once the animation is triggered + return runner; + } + }; + + function endFn() { + close(); + } + + function cancelFn() { + close(true); + } + + function close(rejected) { // jshint ignore:line + // if the promise has been called already then we shouldn't close + // the animation again + if (animationClosed || (animationCompleted && animationPaused)) return; + animationClosed = true; + animationPaused = false; + + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); + } + $$jqLite.removeClass(element, activeClasses); + + blockKeyframeAnimations(node, false); + blockTransitions(node, false); + + forEach(temporaryStyles, function(entry) { + // There is only one way to remove inline style properties entirely from elements. + // By using `removeProperty` this works, but we need to convert camel-cased CSS + // styles down to hyphenated values. + node.style[entry[0]] = ''; + }); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + + if (Object.keys(restoreStyles).length) { + forEach(restoreStyles, function(value, prop) { + value ? node.style.setProperty(prop, value) + : node.style.removeProperty(prop); + }); + } + + // the reason why we have this option is to allow a synchronous closing callback + // that is fired as SOON as the animation ends (when the CSS is removed) or if + // the animation never takes off at all. A good example is a leave animation since + // the element must be removed just after the animation is over or else the element + // will appear on screen for one animation frame causing an overbearing flicker. + if (options.onDone) { + options.onDone(); + } + + if (events && events.length) { + // Remove the transitionend / animationend listener(s) + element.off(events.join(' '), onAnimationProgress); + } + + //Cancel the fallback closing timeout and remove the timer data + var animationTimerData = element.data(ANIMATE_TIMER_KEY); + if (animationTimerData) { + $timeout.cancel(animationTimerData[0].timer); + element.removeData(ANIMATE_TIMER_KEY); + } + + // if the preparation function fails then the promise is not setup + if (runner) { + runner.complete(!rejected); + } + } + + function applyBlocking(duration) { + if (flags.blockTransition) { + blockTransitions(node, duration); + } + + if (flags.blockKeyframeAnimation) { + blockKeyframeAnimations(node, !!duration); + } + } + + function closeAndReturnNoopAnimator() { + runner = new $$AnimateRunner({ + end: endFn, + cancel: cancelFn + }); + + // should flush the cache animation + waitUntilQuiet(noop); + close(); + + return { + $$willAnimate: false, + start: function() { + return runner; + }, + end: endFn + }; + } + + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + + // we now always use `Date.now()` due to the recent changes with + // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info) + var timeStamp = ev.$manualTimeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animationPauseds sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); + } + } + + function start() { + if (animationClosed) return; + if (!node.parentNode) { + close(); + return; + } + + // even though we only pause keyframe animations here the pause flag + // will still happen when transitions are used. Only the transition will + // not be paused since that is not possible. If the animation ends when + // paused then it will not complete until unpaused or cancelled. + var playPause = function(playAnimation) { + if (!animationCompleted) { + animationPaused = !playAnimation; + if (timings.animationDuration) { + var value = blockKeyframeAnimations(node, animationPaused); + animationPaused + ? temporaryStyles.push(value) + : removeFromArray(temporaryStyles, value); + } + } else if (animationPaused && playAnimation) { + animationPaused = false; + close(); + } + }; + + // checking the stagger duration prevents an accidentally cascade of the CSS delay style + // being inherited from the parent. If the transition duration is zero then we can safely + // rely that the delay value is an intentional stagger delay style. + var maxStagger = itemIndex > 0 + && ((timings.transitionDuration && stagger.transitionDuration === 0) || + (timings.animationDuration && stagger.animationDuration === 0)) + && Math.max(stagger.animationDelay, stagger.transitionDelay); + if (maxStagger) { + $timeout(triggerAnimationStart, + Math.floor(maxStagger * itemIndex * ONE_SECOND), + false); + } else { + triggerAnimationStart(); + } + + // this will decorate the existing promise runner with pause/resume methods + runnerHost.resume = function() { + playPause(true); + }; + + runnerHost.pause = function() { + playPause(false); + }; + + function triggerAnimationStart() { + // just incase a stagger animation kicks in when the animation + // itself was cancelled entirely + if (animationClosed) return; + + applyBlocking(false); + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[key] = value; + }); + + applyAnimationClasses(element, options); + $$jqLite.addClass(element, activeClasses); + + if (flags.recalculateTimingStyles) { + fullClassName = node.className + ' ' + preparationClasses; + cacheKey = gcsHashFn(node, fullClassName); + + timings = computeTimings(node, fullClassName, cacheKey); + relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + if (maxDuration === 0) { + close(); + return; + } + + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + } + + if (flags.applyAnimationDelay) { + relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + ? parseFloat(options.delay) + : relativeDelay; + + maxDelay = Math.max(relativeDelay, 0); + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + + if (options.easing) { + var easeProp, easeVal = options.easing; + if (flags.hasTransitions) { + easeProp = TRANSITION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + if (flags.hasAnimations) { + easeProp = ANIMATION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + } + + if (timings.transitionDuration) { + events.push(TRANSITIONEND_EVENT); + } + + if (timings.animationDuration) { + events.push(ANIMATIONEND_EVENT); + } + + startTime = Date.now(); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); + } + } + + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } + + if (events.length) { + element.on(events.join(' '), onAnimationProgress); + } + + if (options.to) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); + } + applyAnimationToStyles(element, options); + } + } + + function onAnimationExpired() { + var animationsData = element.data(ANIMATE_TIMER_KEY); + + // this will be false in the event that the element was + // removed from the DOM (via a leave animation or something + // similar) + if (animationsData) { + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); + } + element.removeData(ANIMATE_TIMER_KEY); + } + } + } + }; + }]; +}]; + +var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateCssDriver'); + + var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; + var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; + + var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; + var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; + + function isDocumentFragment(node) { + return node.parentNode && node.parentNode.nodeType === 11; + } + + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { + + // only browsers that support these properties can render animations + if (!$sniffer.animations && !$sniffer.transitions) return noop; + + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var rootBodyElement = jqLite( + // this is to avoid using something that exists outside of the body + // we also special case the doc fragment case because our unit test code + // appends the $rootElement to the body after the app has been bootstrapped + isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode + ); + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + return function initDriverFn(animationDetails) { + return animationDetails.from && animationDetails.to + ? prepareFromToAnchorAnimation(animationDetails.from, + animationDetails.to, + animationDetails.classes, + animationDetails.anchors) + : prepareRegularAnimation(animationDetails); + }; + + function filterCssClasses(classes) { + //remove all the `ng-` stuff + return classes.replace(/\bng-\S+\b/g, ''); + } + + function getUniqueValues(a, b) { + if (isString(a)) a = a.split(' '); + if (isString(b)) b = b.split(' '); + return a.filter(function(val) { + return b.indexOf(val) === -1; + }).join(' '); + } + + function prepareAnchoredAnimation(classes, outAnchor, inAnchor) { + var clone = jqLite(getDomNode(outAnchor).cloneNode(true)); + var startingClasses = filterCssClasses(getClassVal(clone)); + + outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + + clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME); + + rootBodyElement.append(clone); + + var animatorIn, animatorOut = prepareOutAnimation(); + + // the user may not end up using the `out` animation and + // only making use of the `in` animation or vice-versa. + // In either case we should allow this and not assume the + // animation is over unless both animations are not used. + if (!animatorOut) { + animatorIn = prepareInAnimation(); + if (!animatorIn) { + return end(); + } + } + + var startingAnimator = animatorOut || animatorIn; + + return { + start: function() { + var runner; + + var currentAnimation = startingAnimator.start(); + currentAnimation.done(function() { + currentAnimation = null; + if (!animatorIn) { + animatorIn = prepareInAnimation(); + if (animatorIn) { + currentAnimation = animatorIn.start(); + currentAnimation.done(function() { + currentAnimation = null; + end(); + runner.complete(); + }); + return currentAnimation; + } + } + // in the event that there is no `in` animation + end(); + runner.complete(); + }); + + runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn + }); + + return runner; + + function endFn() { + if (currentAnimation) { + currentAnimation.end(); + } + } + } + }; + + function calculateAnchorStyles(anchor) { + var styles = {}; + + var coords = getDomNode(anchor).getBoundingClientRect(); + + // we iterate directly since safari messes up and doesn't return + // all the keys for the coords object when iterated + forEach(['width','height','top','left'], function(key) { + var value = coords[key]; + switch (key) { + case 'top': + value += bodyNode.scrollTop; + break; + case 'left': + value += bodyNode.scrollLeft; + break; + } + styles[key] = Math.floor(value) + 'px'; + }); + return styles; + } + + function prepareOutAnimation() { + var animator = $animateCss(clone, { + addClass: NG_OUT_ANCHOR_CLASS_NAME, + delay: true, + from: calculateAnchorStyles(outAnchor) + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function getClassVal(element) { + return element.attr('class') || ''; + } + + function prepareInAnimation() { + var endingClasses = filterCssClasses(getClassVal(inAnchor)); + var toAdd = getUniqueValues(endingClasses, startingClasses); + var toRemove = getUniqueValues(startingClasses, endingClasses); + + var animator = $animateCss(clone, { + to: calculateAnchorStyles(inAnchor), + addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd, + removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove, + delay: true + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function end() { + clone.remove(); + outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + } + } + + function prepareFromToAnchorAnimation(from, to, classes, anchors) { + var fromAnimation = prepareRegularAnimation(from, noop); + var toAnimation = prepareRegularAnimation(to, noop); + + var anchorAnimations = []; + forEach(anchors, function(anchor) { + var outElement = anchor['out']; + var inElement = anchor['in']; + var animator = prepareAnchoredAnimation(classes, outElement, inElement); + if (animator) { + anchorAnimations.push(animator); + } + }); + + // no point in doing anything when there are no elements to animate + if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + forEach(anchorAnimations, function(animation) { + animationRunners.push(animation.start()); + }); + + var runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn // CSS-driven animations cannot be cancelled, only ended + }); + + $$AnimateRunner.all(animationRunners, function(status) { + runner.complete(status); + }); + + return runner; + + function endFn() { + forEach(animationRunners, function(runner) { + runner.end(); + }); + } + } + }; + } + + function prepareRegularAnimation(animationDetails) { + var element = animationDetails.element; + var options = animationDetails.options || {}; + + if (animationDetails.structural) { + options.event = animationDetails.event; + options.structural = true; + options.applyClassesEarly = true; + + // we special case the leave animation since we want to ensure that + // the element is removed as soon as the animation is over. Otherwise + // a flicker might appear or the element may not be removed at all + if (animationDetails.event === 'leave') { + options.onDone = options.domOperation; + } + } + + // We assign the preparationClasses as the actual animation event since + // the internals of $animateCss will just suffix the event token values + // with `-active` to trigger the animation. + if (options.preparationClasses) { + options.event = concatWithSpace(options.event, options.preparationClasses); + } + + var animator = $animateCss(element, options); + + // the driver lookup code inside of $$animation attempts to spawn a + // driver one by one until a driver returns a.$$willAnimate animator object. + // $animateCss will always return an object, however, it will pass in + // a flag as a hint as to whether an animation was detected or not + return animator.$$willAnimate ? animator : null; + } + }]; +}]; + +// TODO(matsko): use caching here to speed things up for detection +// TODO(matsko): add documentation +// by the time... + +var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { + this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', + function($injector, $$AnimateRunner, $$jqLite) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + // $animateJs(element, 'enter'); + return function(element, event, classes, options) { + var animationClosed = false; + + // the `classes` argument is optional and if it is not used + // then the classes will be resolved from the element's className + // property as well as options.addClass/options.removeClass. + if (arguments.length === 3 && isObject(classes)) { + options = classes; + classes = null; + } + + options = prepareAnimationOptions(options); + if (!classes) { + classes = element.attr('class') || ''; + if (options.addClass) { + classes += ' ' + options.addClass; + } + if (options.removeClass) { + classes += ' ' + options.removeClass; + } + } + + var classesToAdd = options.addClass; + var classesToRemove = options.removeClass; + + // the lookupAnimations function returns a series of animation objects that are + // matched up with one or more of the CSS classes. These animation objects are + // defined via the module.animation factory function. If nothing is detected then + // we don't return anything which then makes $animation query the next driver. + var animations = lookupAnimations(classes); + var before, after; + if (animations.length) { + var afterFn, beforeFn; + if (event == 'leave') { + beforeFn = 'leave'; + afterFn = 'afterLeave'; // TODO(matsko): get rid of this + } else { + beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1); + afterFn = event; + } + + if (event !== 'enter' && event !== 'move') { + before = packageAnimations(element, event, options, animations, beforeFn); + } + after = packageAnimations(element, event, options, animations, afterFn); + } + + // no matching animations + if (!before && !after) return; + + function applyOptions() { + options.domOperation(); + applyAnimationClasses(element, options); + } + + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } + + var runner; + + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); + } else { + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, + start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); + var closeActiveAnimations; + var chain = []; + + if (before) { + chain.push(function(fn) { + closeActiveAnimations = before(fn); + }); + } + + if (chain.length) { + chain.push(function(fn) { + applyOptions(); + fn(true); + }); + } else { + applyOptions(); + } + + if (after) { + chain.push(function(fn) { + closeActiveAnimations = after(fn); + }); + } + + runner.setHost({ + end: function() { + endAnimations(); + }, + cancel: function() { + endAnimations(true); + } + }); + + $$AnimateRunner.chain(chain, onComplete); + return runner; + + function onComplete(success) { + close(success); + runner.complete(success); + } + + function endAnimations(cancelled) { + if (!animationClosed) { + (closeActiveAnimations || noop)(cancelled); + onComplete(cancelled); + } + } + } + }; + + function executeAnimationFn(fn, element, event, options, onDone) { + var args; + switch (event) { + case 'animate': + args = [element, options.from, options.to, onDone]; + break; + + case 'setClass': + args = [element, classesToAdd, classesToRemove, onDone]; + break; + + case 'addClass': + args = [element, classesToAdd, onDone]; + break; + + case 'removeClass': + args = [element, classesToRemove, onDone]; + break; + + default: + args = [element, onDone]; + break; + } + + args.push(options); + + var value = fn.apply(fn, args); + if (value) { + if (isFunction(value.start)) { + value = value.start(); + } + + if (value instanceof $$AnimateRunner) { + value.done(onDone); + } else if (isFunction(value)) { + // optional onEnd / onCancel callback + return value; + } + } + + return noop; + } + + function groupEventedAnimations(element, event, options, animations, fnName) { + var operations = []; + forEach(animations, function(ani) { + var animation = ani[fnName]; + if (!animation) return; + + // note that all of these animations will run in parallel + operations.push(function() { + var runner; + var endProgressCb; + + var resolved = false; + var onAnimationComplete = function(rejected) { + if (!resolved) { + resolved = true; + (endProgressCb || noop)(rejected); + runner.complete(!rejected); + } + }; + + runner = new $$AnimateRunner({ + end: function() { + onAnimationComplete(); + }, + cancel: function() { + onAnimationComplete(true); + } + }); + + endProgressCb = executeAnimationFn(animation, element, event, options, function(result) { + var cancelled = result === false; + onAnimationComplete(cancelled); + }); + + return runner; + }); + }); + + return operations; + } + + function packageAnimations(element, event, options, animations, fnName) { + var operations = groupEventedAnimations(element, event, options, animations, fnName); + if (operations.length === 0) { + var a,b; + if (fnName === 'beforeSetClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); + } else if (fnName === 'setClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass'); + } + + if (a) { + operations = operations.concat(a); + } + if (b) { + operations = operations.concat(b); + } + } + + if (operations.length === 0) return; + + // TODO(matsko): add documentation + return function startAnimation(callback) { + var runners = []; + if (operations.length) { + forEach(operations, function(animateFn) { + runners.push(animateFn()); + }); + } + + runners.length ? $$AnimateRunner.all(runners, callback) : callback(); + + return function endFn(reject) { + forEach(runners, function(runner) { + reject ? runner.cancel() : runner.end(); + }); + }; + }; + } + }; + + function lookupAnimations(classes) { + classes = isArray(classes) ? classes : classes.split(' '); + var matches = [], flagMap = {}; + for (var i=0; i < classes.length; i++) { + var klass = classes[i], + animationFactory = $animateProvider.$$registeredAnimations[klass]; + if (animationFactory && !flagMap[klass]) { + matches.push($injector.get(animationFactory)); + flagMap[klass] = true; + } + } + return matches; + } + }]; +}]; + +var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateJsDriver'); + this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { + return function initDriverFn(animationDetails) { + if (animationDetails.from && animationDetails.to) { + var fromAnimation = prepareAnimation(animationDetails.from); + var toAnimation = prepareAnimation(animationDetails.to); + if (!fromAnimation && !toAnimation) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + $$AnimateRunner.all(animationRunners, done); + + var runner = new $$AnimateRunner({ + end: endFnFactory(), + cancel: endFnFactory() + }); + + return runner; + + function endFnFactory() { + return function() { + forEach(animationRunners, function(runner) { + // at this point we cannot cancel animations for groups just yet. 1.5+ + runner.end(); + }); + }; + } + + function done(status) { + runner.complete(status); + } + } + }; + } else { + return prepareAnimation(animationDetails); + } + }; + + function prepareAnimation(animationDetails) { + // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations + var element = animationDetails.element; + var event = animationDetails.event; + var options = animationDetails.options; + var classes = animationDetails.classes; + return $$animateJs(element, event, classes, options); + } + }]; +}]; + +var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; +var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; +var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { + var PRE_DIGEST_STATE = 1; + var RUNNING_STATE = 2; + var ONE_SPACE = ' '; + + var rules = this.rules = { + skip: [], + cancel: [], + join: [] + }; + + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + return rules[ruleType].some(function(fn) { + return fn(element, currentAnimation, previousAnimation); + }); + } + + function hasAnimationClasses(animation, and) { + var a = (animation.addClass || '').length > 0; + var b = (animation.removeClass || '').length > 0; + return and ? a && b : a || b; + } + + rules.join.push(function(element, newAnimation, currentAnimation) { + // if the new animation is class-based then we can just tack that on + return !newAnimation.structural && hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // there is no need to animate anything if no classes are being added and + // there is no structural animation that will be triggered + return !newAnimation.structural && !hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // why should we trigger a new structural animation if the element will + // be removed from the DOM anyway? + return currentAnimation.event == 'leave' && newAnimation.structural; + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // if there is an ongoing current animation then don't even bother running the class-based animation + return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // there can never be two structural animations running at the same time + return currentAnimation.structural && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // if the previous animation is already running, but the new animation will + // be triggered, but the new animation is structural + return currentAnimation.state === RUNNING_STATE && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + var nA = newAnimation.addClass; + var nR = newAnimation.removeClass; + var cA = currentAnimation.addClass; + var cR = currentAnimation.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } + + return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); + }); + + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + function($$rAF, $rootScope, $rootElement, $document, $$HashMap, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + + var activeAnimationsLookup = new $$HashMap(); + var disabledElementsLookup = new $$HashMap(); + var animationsEnabled = null; + + function postDigestTaskFactory() { + var postDigestCalled = false; + return function(fn) { + // we only issue a call to postDigest before + // it has first passed. This prevents any callbacks + // from not firing once the animation has completed + // since it will be out of the digest cycle. + if (postDigestCalled) { + fn(); + } else { + $rootScope.$$postDigest(function() { + postDigestCalled = true; + fn(); + }); + } + }; + } + + // Wait until all directive and route-related templates are downloaded and + // compiled. The $templateRequest.totalPendingRequests variable keeps track of + // all of the remote templates being currently downloaded. If there are no + // templates currently downloading then the watcher will still fire anyway. + var deregisterWatch = $rootScope.$watch( + function() { return $templateRequest.totalPendingRequests === 0; }, + function(isEmpty) { + if (!isEmpty) return; + deregisterWatch(); + + // Now that all templates have been downloaded, $animate will wait until + // the post digest queue is empty before enabling animations. By having two + // calls to $postDigest calls we can ensure that the flag is enabled at the + // very end of the post digest queue. Since all of the animations in $animate + // use $postDigest, it's important that the code below executes at the end. + // This basically means that the page is fully downloaded and compiled before + // any animations are triggered. + $rootScope.$$postDigest(function() { + $rootScope.$$postDigest(function() { + // we check for null directly in the event that the application already called + // .enabled() with whatever arguments that it provided it with + if (animationsEnabled === null) { + animationsEnabled = true; + } + }); + }); + } + ); + + var callbackRegistry = {}; + + // remember that the classNameFilter is set during the provider/config + // stage therefore we can optimize here and setup a helper function + var classNameFilter = $animateProvider.classNameFilter(); + var isAnimatableClassName = !classNameFilter + ? function() { return true; } + : function(className) { + return classNameFilter.test(className); + }; + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function normalizeAnimationDetails(element, animation) { + return mergeAnimationDetails(element, animation, {}); + } + + // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var contains = Node.prototype.contains || function(arg) { + // jshint bitwise: false + return this === arg || !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true + }; + + function findCallbacks(parent, element, event) { + var targetNode = getDomNode(element); + var targetParentNode = getDomNode(parent); + + var matches = []; + var entries = callbackRegistry[event]; + if (entries) { + forEach(entries, function(entry) { + if (contains.call(entry.node, targetNode)) { + matches.push(entry.callback); + } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { + matches.push(entry.callback); + } + }); + } + + return matches; + } + + return { + on: function(event, container, callback) { + var node = extractElementNode(container); + callbackRegistry[event] = callbackRegistry[event] || []; + callbackRegistry[event].push({ + node: node, + callback: callback + }); + }, + + off: function(event, container, callback) { + var entries = callbackRegistry[event]; + if (!entries) return; + + callbackRegistry[event] = arguments.length === 1 + ? null + : filterFromRegistry(entries, container, callback); + + function filterFromRegistry(list, matchContainer, matchCallback) { + var containerNode = extractElementNode(matchContainer); + return list.filter(function(entry) { + var isMatch = entry.node === containerNode && + (!matchCallback || entry.callback === matchCallback); + return !isMatch; + }); + } + }, + + pin: function(element, parentElement) { + assertArg(isElement(element), 'element', 'not an element'); + assertArg(isElement(parentElement), 'parentElement', 'not an element'); + element.data(NG_ANIMATE_PIN_DATA, parentElement); + }, + + push: function(element, event, options, domOperation) { + options = options || {}; + options.domOperation = domOperation; + return queueAnimation(element, event, options); + }, + + // this method has four signatures: + // () - global getter + // (bool) - global setter + // (element) - element getter + // (element, bool) - element setter + enabled: function(element, bool) { + var argCount = arguments.length; + + if (argCount === 0) { + // () - Global getter + bool = !!animationsEnabled; + } else { + var hasElement = isElement(element); + + if (!hasElement) { + // (bool) - Global setter + bool = animationsEnabled = !!element; + } else { + var node = getDomNode(element); + var recordExists = disabledElementsLookup.get(node); + + if (argCount === 1) { + // (element) - Element getter + bool = !recordExists; + } else { + // (element, bool) - Element setter + disabledElementsLookup.put(node, !bool); + } + } + } + + return bool; + } + }; + + function queueAnimation(element, event, initialOptions) { + // we always make a copy of the options since + // there should never be any side effects on + // the input data when running `$animateCss`. + var options = copy(initialOptions); + + var node, parent; + element = stripCommentsFromElement(element); + if (element) { + node = getDomNode(element); + parent = element.parent(); + } + + options = prepareAnimationOptions(options); + + // we create a fake runner with a working promise. + // These methods will become available after the digest has passed + var runner = new $$AnimateRunner(); + + // this is used to trigger callbacks in postDigest mode + var runInNextPostDigestOrNow = postDigestTaskFactory(); + + if (isArray(options.addClass)) { + options.addClass = options.addClass.join(' '); + } + + if (options.addClass && !isString(options.addClass)) { + options.addClass = null; + } + + if (isArray(options.removeClass)) { + options.removeClass = options.removeClass.join(' '); + } + + if (options.removeClass && !isString(options.removeClass)) { + options.removeClass = null; + } + + if (options.from && !isObject(options.from)) { + options.from = null; + } + + if (options.to && !isObject(options.to)) { + options.to = null; + } + + // there are situations where a directive issues an animation for + // a jqLite wrapper that contains only comment nodes... If this + // happens then there is no way we can perform an animation + if (!node) { + close(); + return runner; + } + + var className = [node.className, options.addClass, options.removeClass].join(' '); + if (!isAnimatableClassName(className)) { + close(); + return runner; + } + + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // this is a hard disable of all animations for the application or on + // the element itself, therefore there is no need to continue further + // past this point if not enabled + // Animations are also disabled if the document is currently hidden (page is not visible + // to the user), because browsers slow down or do not flush calls to requestAnimationFrame + var skipAnimations = !animationsEnabled || $document[0].hidden || disabledElementsLookup.get(node); + var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; + var hasExistingAnimation = !!existingAnimation.state; + + // there is no point in traversing the same collection of parent ancestors if a followup + // animation will be run on the same element that already did all that checking work + if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(element, parent, event); + } + + if (skipAnimations) { + close(); + return runner; + } + + if (isStructural) { + closeChildAnimations(element); + } + + var newAnimation = { + structural: isStructural, + element: element, + event: event, + addClass: options.addClass, + removeClass: options.removeClass, + close: close, + options: options, + runner: runner + }; + + if (hasExistingAnimation) { + var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + if (skipAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + close(); + return runner; + } else { + mergeAnimationDetails(element, existingAnimation, newAnimation); + return existingAnimation.runner; + } + } + var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + if (cancelAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + // this will end the animation right away and it is safe + // to do so since the animation is already running and the + // runner callback code will run in async + existingAnimation.runner.end(); + } else if (existingAnimation.structural) { + // this means that the animation is queued into a digest, but + // hasn't started yet. Therefore it is safe to run the close + // method which will call the runner methods in async. + existingAnimation.close(); + } else { + // this will merge the new animation options into existing animation options + mergeAnimationDetails(element, existingAnimation, newAnimation); + + return existingAnimation.runner; + } + } else { + // a joined animation means that this animation will take over the existing one + // so an example would involve a leave animation taking over an enter. Then when + // the postDigest kicks in the enter will be ignored. + var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); + if (joinAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + normalizeAnimationDetails(element, newAnimation); + } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + + event = newAnimation.event = existingAnimation.event; + options = mergeAnimationDetails(element, existingAnimation, newAnimation); + + //we return the same runner since only the option values of this animation will + //be fed into the `existingAnimation`. + return existingAnimation.runner; + } + } + } + } else { + // normalization in this case means that it removes redundant CSS classes that + // already exist (addClass) or do not exist (removeClass) on the element + normalizeAnimationDetails(element, newAnimation); + } + + // when the options are merged and cleaned up we may end up not having to do + // an animation at all, therefore we should check this before issuing a post + // digest callback. Structural animations will always run no matter what. + var isValidAnimation = newAnimation.structural; + if (!isValidAnimation) { + // animate (from/to) can be quickly checked first, otherwise we check if any classes are present + isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) + || hasAnimationClasses(newAnimation); + } + + if (!isValidAnimation) { + close(); + clearElementAnimationState(element); + return runner; + } + + // the counter keeps track of cancelled animations + var counter = (existingAnimation.counter || 0) + 1; + newAnimation.counter = counter; + + markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + + $rootScope.$$postDigest(function() { + var animationDetails = activeAnimationsLookup.get(node); + var animationCancelled = !animationDetails; + animationDetails = animationDetails || {}; + + // if addClass/removeClass is called before something like enter then the + // registered parent element may not be present. The code below will ensure + // that a final value for parent element is obtained + var parentElement = element.parent() || []; + + // animate/structural/class-based animations all have requirements. Otherwise there + // is no point in performing an animation. The parent node must also be set. + var isValidAnimation = parentElement.length > 0 + && (animationDetails.event === 'animate' + || animationDetails.structural + || hasAnimationClasses(animationDetails)); + + // this means that the previous animation was cancelled + // even if the follow-up animation is the same event + if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) { + // if another animation did not take over then we need + // to make sure that the domOperation and options are + // handled accordingly + if (animationCancelled) { + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + } + + // if the event changed from something like enter to leave then we do + // it, otherwise if it's the same then the end result will be the same too + if (animationCancelled || (isStructural && animationDetails.event !== event)) { + options.domOperation(); + runner.end(); + } + + // in the event that the element animation was not cancelled or a follow-up animation + // isn't allowed to animate from here then we need to clear the state of the element + // so that any future animations won't read the expired animation data. + if (!isValidAnimation) { + clearElementAnimationState(element); + } + + return; + } + + // this combined multiple class to addClass / removeClass into a setClass event + // so long as a structural event did not take over the animation + event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) + ? 'setClass' + : animationDetails.event; + + markElementAnimationState(element, RUNNING_STATE); + var realRunner = $$animation(element, event, animationDetails.options); + + realRunner.done(function(status) { + close(!status); + var animationDetails = activeAnimationsLookup.get(node); + if (animationDetails && animationDetails.counter === counter) { + clearElementAnimationState(getDomNode(element)); + } + notifyProgress(runner, event, 'close', {}); + }); + + // this will update the runner's flow-control events based on + // the `realRunner` object. + runner.setHost(realRunner); + notifyProgress(runner, event, 'start', {}); + }); + + return runner; + + function notifyProgress(runner, event, phase, data) { + runInNextPostDigestOrNow(function() { + var callbacks = findCallbacks(parent, element, event); + if (callbacks.length) { + // do not optimize this call here to RAF because + // we don't know how heavy the callback code here will + // be and if this code is buffered then this can + // lead to a performance regression. + $$rAF(function() { + forEach(callbacks, function(callback) { + callback(element, phase, data); + }); + }); + } + }); + runner.progress(event, phase, data); + } + + function close(reject) { // jshint ignore:line + clearGeneratedClasses(element, options); + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + runner.complete(!reject); + } + } + + function closeChildAnimations(element) { + var node = getDomNode(element); + var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); + forEach(children, function(child) { + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var animationDetails = activeAnimationsLookup.get(child); + if (animationDetails) { + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: + activeAnimationsLookup.remove(child); + break; + } + } + }); + } + + function clearElementAnimationState(element) { + var node = getDomNode(element); + node.removeAttribute(NG_ANIMATE_ATTR_NAME); + activeAnimationsLookup.remove(node); + } + + function isMatchingElement(nodeOrElmA, nodeOrElmB) { + return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); + } + + /** + * This fn returns false if any of the following is true: + * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed + * b) a parent element has an ongoing structural animation, and animateChildren is false + * c) the element is not a child of the body + * d) the element is not a child of the $rootElement + */ + function areAnimationsAllowed(element, parentElement, event) { + var bodyElement = jqLite($document[0].body); + var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); + var parentAnimationDetected = false; + var animateChildren; + var elementDisabled = disabledElementsLookup.get(getDomNode(element)); + + var parentHost = element.data(NG_ANIMATE_PIN_DATA); + if (parentHost) { + parentElement = parentHost; + } + + while (parentElement && parentElement.length) { + if (!rootElementDetected) { + // angular doesn't want to attempt to animate elements outside of the application + // therefore we need to ensure that the rootElement is an ancestor of the current element + rootElementDetected = isMatchingElement(parentElement, $rootElement); + } + + var parentNode = parentElement[0]; + if (parentNode.nodeType !== ELEMENT_NODE) { + // no point in inspecting the #document element + break; + } + + var details = activeAnimationsLookup.get(parentNode) || {}; + // either an enter, leave or move animation will commence + // therefore we can't allow any animations to take place + // but if a parent animation is class-based then that's ok + if (!parentAnimationDetected) { + var parentElementDisabled = disabledElementsLookup.get(parentNode); + + if (parentElementDisabled === true && elementDisabled !== false) { + // disable animations if the user hasn't explicitly enabled animations on the + // current element + elementDisabled = true; + // element is disabled via parent element, no need to check anything else + break; + } else if (parentElementDisabled === false) { + elementDisabled = false; + } + parentAnimationDetected = details.structural; + } + + if (isUndefined(animateChildren) || animateChildren === true) { + var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA); + if (isDefined(value)) { + animateChildren = value; + } + } + + // there is no need to continue traversing at this point + if (parentAnimationDetected && animateChildren === false) break; + + if (!bodyElementDetected) { + // we also need to ensure that the element is or will be a part of the body element + // otherwise it is pointless to even issue an animation to be rendered + bodyElementDetected = isMatchingElement(parentElement, bodyElement); + } + + if (bodyElementDetected && rootElementDetected) { + // If both body and root have been found, any other checks are pointless, + // as no animation data should live outside the application + break; + } + + if (!rootElementDetected) { + // If no rootElement is detected, check if the parentElement is pinned to another element + parentHost = parentElement.data(NG_ANIMATE_PIN_DATA); + if (parentHost) { + // The pin target element becomes the next parent element + parentElement = parentHost; + continue; + } + } + + parentElement = parentElement.parent(); + } + + var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; + return allowAnimation && rootElementDetected && bodyElementDetected; + } + + function markElementAnimationState(element, state, details) { + details = details || {}; + details.state = state; + + var node = getDomNode(element); + node.setAttribute(NG_ANIMATE_ATTR_NAME, state); + + var oldValue = activeAnimationsLookup.get(node); + var newValue = oldValue + ? extend(oldValue, details) + : details; + activeAnimationsLookup.put(node, newValue); + } + }]; +}]; + +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; + + var drivers = this.drivers = []; + + var RUNNER_STORAGE_KEY = '$$animationRunner'; + + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } + + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); + } + + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } + + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function sortAnimations(animations) { + var tree = { children: [] }; + var i, lookup = new $$HashMap(); + + // this is done first beforehand so that the hashmap + // is filled with a list of the elements that will be animated + for (i = 0; i < animations.length; i++) { + var animation = animations[i]; + lookup.put(animation.domNode, animations[i] = { + domNode: animation.domNode, + fn: animation.fn, + children: [] + }); + } + + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); + } + + return flatten(tree); + + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; + + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); + + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; + } + parentNode = parentNode.parentNode; + } + + (parentEntry || tree).children.push(entry); + return entry; + } + + function flatten(tree) { + var result = []; + var queue = []; + var i; + + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } + + var remainingLevelEntries = queue.length; + var nextLevelEntries = 0; + var row = []; + + for (i = 0; i < queue.length; i++) { + var entry = queue[i]; + if (remainingLevelEntries <= 0) { + remainingLevelEntries = nextLevelEntries; + nextLevelEntries = 0; + result.push(row); + row = []; + } + row.push(entry.fn); + entry.children.forEach(function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } + + if (row.length) { + result.push(row); + } + + return result; + } + } + + // TODO(matsko): document the signature in a better way + return function(element, event, options) { + options = prepareAnimationOptions(options); + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // there is no animation at the current moment, however + // these runner methods will get later updated with the + // methods leading into the driver's end/cancel methods + // for now they just stop the animation from starting + var runner = new $$AnimateRunner({ + end: function() { close(); }, + cancel: function() { close(true); } + }); + + if (!drivers.length) { + close(); + return runner; + } + + setRunner(element, runner); + + var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); + var tempClasses = options.tempClasses; + if (tempClasses) { + classes += ' ' + tempClasses; + options.tempClasses = null; + } + + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); + } + + animationQueue.push({ + // this data is used by the postDigest code and passed into + // the driver step function + element: element, + classes: classes, + event: event, + structural: isStructural, + options: options, + beforeStart: beforeStart, + close: close + }); + + element.on('$destroy', handleDestroyedElement); + + // we only want there to be one function called within the post digest + // block. This way we can group animations for all the animations that + // were apart of the same postDigest flush call. + if (animationQueue.length > 1) return runner; + + $rootScope.$$postDigest(function() { + var animations = []; + forEach(animationQueue, function(entry) { + // the element was destroyed early on which removed the runner + // form its storage. This means we can't animate this element + // at all and it already has been closed due to destruction. + if (getRunner(entry.element)) { + animations.push(entry); + } else { + entry.close(); + } + }); + + // now any future animations will be in another postDigest + animationQueue.length = 0; + + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; + + forEach(groupedAnimations, function(animationEntry) { + toBeSortedAnimations.push({ + domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + fn: function triggerAnimationStart() { + // it's important that we apply the `ng-animate` CSS class and the + // temporary classes before we do any driver invoking since these + // CSS classes may be required for proper CSS detection. + animationEntry.beforeStart(); + + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that the element was removed before the digest runs or + // during the RAF sequencing then we should not trigger the animation. + var targetElement = animationEntry.anchors + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; + } + } + + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } + } + }); + }); + + // we need to sort each of the animations in order of parent to child + // relationships. This ensures that the child classes are applied at the + // right time. + $$rAFScheduler(sortAnimations(toBeSortedAnimations)); + }); + + return runner; + + // TODO(matsko): change to reference nodes + function getAnchorNodes(node) { + var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']'; + var items = node.hasAttribute(NG_ANIMATE_REF_ATTR) + ? [node] + : node.querySelectorAll(SELECTOR); + var anchors = []; + forEach(items, function(node) { + var attr = node.getAttribute(NG_ANIMATE_REF_ATTR); + if (attr && attr.length) { + anchors.push(node); + } + }); + return anchors; + } + + function groupAnimations(animations) { + var preparedAnimations = []; + var refLookup = {}; + forEach(animations, function(animation, index) { + var element = animation.element; + var node = getDomNode(element); + var event = animation.event; + var enterOrMove = ['enter', 'move'].indexOf(event) >= 0; + var anchorNodes = animation.structural ? getAnchorNodes(node) : []; + + if (anchorNodes.length) { + var direction = enterOrMove ? 'to' : 'from'; + + forEach(anchorNodes, function(anchor) { + var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR); + refLookup[key] = refLookup[key] || {}; + refLookup[key][direction] = { + animationID: index, + element: jqLite(anchor) + }; + }); + } else { + preparedAnimations.push(animation); + } + }); + + var usedIndicesLookup = {}; + var anchorGroups = {}; + forEach(refLookup, function(operations, key) { + var from = operations.from; + var to = operations.to; + + if (!from || !to) { + // only one of these is set therefore we can't have an + // anchor animation since all three pieces are required + var index = from ? from.animationID : to.animationID; + var indexKey = index.toString(); + if (!usedIndicesLookup[indexKey]) { + usedIndicesLookup[indexKey] = true; + preparedAnimations.push(animations[index]); + } + return; + } + + var fromAnimation = animations[from.animationID]; + var toAnimation = animations[to.animationID]; + var lookupKey = from.animationID.toString(); + if (!anchorGroups[lookupKey]) { + var group = anchorGroups[lookupKey] = { + structural: true, + beforeStart: function() { + fromAnimation.beforeStart(); + toAnimation.beforeStart(); + }, + close: function() { + fromAnimation.close(); + toAnimation.close(); + }, + classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes), + from: fromAnimation, + to: toAnimation, + anchors: [] // TODO(matsko): change to reference nodes + }; + + // the anchor animations require that the from and to elements both have at least + // one shared CSS class which effectively marries the two elements together to use + // the same animation driver and to properly sequence the anchor animation. + if (group.classes.length) { + preparedAnimations.push(group); + } else { + preparedAnimations.push(fromAnimation); + preparedAnimations.push(toAnimation); + } + } + + anchorGroups[lookupKey].anchors.push({ + 'out': from.element, 'in': to.element + }); + }); + + return preparedAnimations; + } + + function cssClassesIntersection(a,b) { + a = a.split(' '); + b = b.split(' '); + var matches = []; + + for (var i = 0; i < a.length; i++) { + var aa = a[i]; + if (aa.substring(0,3) === 'ng-') continue; + + for (var j = 0; j < b.length; j++) { + if (aa === b[j]) { + matches.push(aa); + break; + } + } + } + + return matches.join(' '); + } + + function invokeFirstDriver(animationDetails) { + // we loop in reverse order since the more general drivers (like CSS and JS) + // may attempt more elements, but custom drivers are more particular + for (var i = drivers.length - 1; i >= 0; i--) { + var driverName = drivers[i]; + if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check + + var factory = $injector.get(driverName); + var driver = factory(animationDetails); + if (driver) { + return driver; + } + } + } + + function beforeStart() { + element.addClass(NG_ANIMATE_CLASSNAME); + if (tempClasses) { + $$jqLite.addClass(element, tempClasses); + } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } + } + + function updateAnimationRunners(animation, newRunner) { + if (animation.from && animation.to) { + update(animation.from.element); + update(animation.to.element); + } else { + update(animation.element); + } + + function update(element) { + getRunner(element).setHost(newRunner); + } + } + + function handleDestroyedElement() { + var runner = getRunner(element); + if (runner && (event !== 'leave' || !options.$$domOperationFired)) { + runner.end(); + } + } + + function close(rejected) { // jshint ignore:line + element.off('$destroy', handleDestroyedElement); + removeRunner(element); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + + if (tempClasses) { + $$jqLite.removeClass(element, tempClasses); + } + + element.removeClass(NG_ANIMATE_CLASSNAME); + runner.complete(!rejected); + } + }; + }]; +}]; + +/** + * @ngdoc directive + * @name ngAnimateSwap + * @restrict A + * @scope + * + * @description + * + * ngAnimateSwap is a animation-oriented directive that allows for the container to + * be removed and entered in whenever the associated expression changes. A + * common usecase for this directive is a rotating banner component which + * contains one image being present at a time. When the active image changes + * then the old image will perform a `leave` animation and the new element + * will be inserted via an `enter` animation. + * + * @example + * + * + *
+ *
+ * {{ number }} + *
+ *
+ *
+ * + * angular.module('ngAnimateSwapExample', ['ngAnimate']) + * .controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) { + * $scope.number = 0; + * $interval(function() { + * $scope.number++; + * }, 1000); + * + * var colors = ['red','blue','green','yellow','orange']; + * $scope.colorClass = function(number) { + * return colors[number % colors.length]; + * }; + * }]); + * + * + * .container { + * height:250px; + * width:250px; + * position:relative; + * overflow:hidden; + * border:2px solid black; + * } + * .container .cell { + * font-size:150px; + * text-align:center; + * line-height:250px; + * position:absolute; + * top:0; + * left:0; + * right:0; + * border-bottom:2px solid black; + * } + * .swap-animation.ng-enter, .swap-animation.ng-leave { + * transition:0.5s linear all; + * } + * .swap-animation.ng-enter { + * top:-250px; + * } + * .swap-animation.ng-enter-active { + * top:0px; + * } + * .swap-animation.ng-leave { + * top:0px; + * } + * .swap-animation.ng-leave-active { + * top:250px; + * } + * .red { background:red; } + * .green { background:green; } + * .blue { background:blue; } + * .yellow { background:yellow; } + * .orange { background:orange; } + * + *
+ */ +var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) { + return { + restrict: 'A', + transclude: 'element', + terminal: true, + priority: 600, // we use 600 here to ensure that the directive is caught before others + link: function(scope, $element, attrs, ctrl, $transclude) { + var previousElement, previousScope; + scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) { + if (previousElement) { + $animate.leave(previousElement); + } + if (previousScope) { + previousScope.$destroy(); + previousScope = null; + } + if (value || value === 0) { + previousScope = scope.$new(); + $transclude(previousScope, function(element) { + previousElement = element; + $animate.enter(element, null, $element); + }); + } + }); + } + }; +}]; + +/* global angularAnimateModule: true, + + ngAnimateSwapDirective, + $$AnimateAsyncRunFactory, + $$rAFSchedulerFactory, + $$AnimateChildrenDirective, + $$AnimateQueueProvider, + $$AnimationProvider, + $AnimateCssProvider, + $$AnimateCssDriverProvider, + $$AnimateJsProvider, + $$AnimateJsDriverProvider, +*/ + +/** + * @ngdoc module + * @name ngAnimate + * @description + * + * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app. + * + *
+ * + * # Usage + * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based + * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For + * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within + * the HTML element that the animation will be triggered on. + * + * ## Directive Support + * The following directives are "animation aware": + * + * | Directive | Supported Animations | + * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * + * (More information can be found by visiting each the documentation associated with each directive.) + * + * ## CSS-based Animations + * + * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML + * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation. + * + * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: + * + * ```html + *
+ * Fade me in out + *
+ * + * + * ``` + * + * Notice the CSS class **fade**? We can now create the CSS transition code that references this class: + * + * ```css + * /* The starting CSS styles for the enter animation */ + * .fade.ng-enter { + * transition:0.5s linear all; + * opacity:0; + * } + * + * /* The finishing CSS styles for the enter animation */ + * .fade.ng-enter.ng-enter-active { + * opacity:1; + * } + * ``` + * + * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two + * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition + * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards. + * + * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions: + * + * ```css + * /* now the element will fade out before it is removed from the DOM */ + * .fade.ng-leave { + * transition:0.5s linear all; + * opacity:1; + * } + * .fade.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class: + * + * ```css + * /* there is no need to define anything inside of the destination + * CSS class since the keyframe will take charge of the animation */ + * .fade.ng-leave { + * animation: my_fade_animation 0.5s linear; + * -webkit-animation: my_fade_animation 0.5s linear; + * } + * + * @keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * + * @-webkit-keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * ``` + * + * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element. + * + * ### CSS Class-based Animations + * + * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different + * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added + * and removed. + * + * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class: + * + * ```html + *
+ * Show and hide me + *
+ * + * + * + * ``` + * + * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since + * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest. + * + * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation + * with CSS styles. + * + * ```html + *
+ * Highlight this box + *
+ * + * + * + * ``` + * + * We can also make use of CSS keyframes by placing them within the CSS classes. + * + * + * ### CSS Staggering Animations + * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a + * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be + * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for + * the animation. The style property expected within the stagger class can either be a **transition-delay** or an + * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). + * + * ```css + * .my-animation.ng-enter { + * /* standard transition code */ + * transition: 1s linear all; + * opacity:0; + * } + * .my-animation.ng-enter-stagger { + * /* this will have a 100ms delay between each successive leave animation */ + * transition-delay: 0.1s; + * + * /* As of 1.4.4, this must always be set: it signals ngAnimate + * to not accidentally inherit a delay property from another CSS class */ + * transition-duration: 0s; + * } + * .my-animation.ng-enter.ng-enter-active { + * /* standard transition styles */ + * opacity:1; + * } + * ``` + * + * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations + * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this + * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation + * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired. + * + * The following code will issue the **ng-leave-stagger** event on the element provided: + * + * ```js + * var kids = parent.children(); + * + * $animate.leave(kids[0]); //stagger index=0 + * $animate.leave(kids[1]); //stagger index=1 + * $animate.leave(kids[2]); //stagger index=2 + * $animate.leave(kids[3]); //stagger index=3 + * $animate.leave(kids[4]); //stagger index=4 + * + * window.requestAnimationFrame(function() { + * //stagger has reset itself + * $animate.leave(kids[5]); //stagger index=0 + * $animate.leave(kids[6]); //stagger index=1 + * + * $scope.$digest(); + * }); + * ``` + * + * Stagger animations are currently only supported within CSS-defined animations. + * + * ### The `ng-animate` CSS class + * + * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation. + * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations). + * + * Therefore, animations can be applied to an element using this temporary class directly via CSS. + * + * ```css + * .zipper.ng-animate { + * transition:0.5s linear all; + * } + * .zipper.ng-enter { + * opacity:0; + * } + * .zipper.ng-enter.ng-enter-active { + * opacity:1; + * } + * .zipper.ng-leave { + * opacity:1; + * } + * .zipper.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove + * the CSS class once an animation has completed.) + * + * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + *
+ *
+ *
+ *
+ *
+ * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * + * ## JavaScript-based Animations + * + * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared + * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the + * `module.animation()` module function we can register the animation. + * + * Let's see an example of a enter/leave animation using `ngRepeat`: + * + * ```html + *
+ * {{ item }} + *
+ * ``` + * + * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`: + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * // make note that other events (like addClass/removeClass) + * // have different function input parameters + * enter: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * + * // remember to call doneFn so that angular + * // knows that the animation has concluded + * }, + * + * move: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * }, + * + * leave: function(element, doneFn) { + * jQuery(element).fadeOut(1000, doneFn); + * } + * } + * }]); + * ``` + * + * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as + * greensock.js and velocity.js. + * + * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define + * our animations inside of the same registered animation, however, the function input arguments are a bit different: + * + * ```html + *
+ * this box is moody + *
+ * + * + * + * ``` + * + * ```js + * myModule.animation('.colorful', [function() { + * return { + * addClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * removeClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * setClass: function(element, addedClass, removedClass, doneFn) { + * // do some cool animation and call the doneFn + * } + * } + * }]); + * ``` + * + * ## CSS + JS Animations Together + * + * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular, + * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking + * charge of the animation**: + * + * ```html + *
+ * Slide in and out + *
+ * ``` + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * enter: function(element, doneFn) { + * jQuery(element).slideIn(1000, doneFn); + * } + * } + * }]); + * ``` + * + * ```css + * .slide.ng-enter { + * transition:0.5s linear all; + * transform:translateY(-100px); + * } + * .slide.ng-enter.ng-enter-active { + * transform:translateY(0); + * } + * ``` + * + * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the + * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from + * our own JS-based animation code: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { +* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. + * return $animateCss(element, { + * event: 'enter', + * structural: true + * }); + * } + * } + * }]); + * ``` + * + * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. + * + * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or + * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that + * data into `$animateCss` directly: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { + * return $animateCss(element, { + * event: 'enter', + * structural: true, + * addClass: 'maroon-setting', + * from: { height:0 }, + * to: { height: 200 } + * }); + * } + * } + * }]); + * ``` + * + * Now we can fill in the rest via our transition CSS code: + * + * ```css + * /* the transition tells ngAnimate to make the animation happen */ + * .slide.ng-enter { transition:0.5s linear all; } + * + * /* this extra CSS class will be absorbed into the transition + * since the $animateCss code is adding the class */ + * .maroon-setting { background:red; } + * ``` + * + * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over. + * + * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}. + * + * ## Animation Anchoring (via `ng-animate-ref`) + * + * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between + * structural areas of an application (like views) by pairing up elements using an attribute + * called `ng-animate-ref`. + * + * Let's say for example we have two views that are managed by `ng-view` and we want to show + * that there is a relationship between two components situated in within these views. By using the + * `ng-animate-ref` attribute we can identify that the two components are paired together and we + * can then attach an animation, which is triggered when the view changes. + * + * Say for example we have the following template code: + * + * ```html + * + *
+ *
+ * + * + *
+ * + * + * + * + * + * ``` + * + * Now, when the view changes (once the link is clicked), ngAnimate will examine the + * HTML contents to see if there is a match reference between any components in the view + * that is leaving and the view that is entering. It will scan both the view which is being + * removed (leave) and inserted (enter) to see if there are any paired DOM elements that + * contain a matching ref value. + * + * The two images match since they share the same ref value. ngAnimate will now create a + * transport element (which is a clone of the first image element) and it will then attempt + * to animate to the position of the second image element in the next view. For the animation to + * work a special CSS class called `ng-anchor` will be added to the transported element. + * + * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then + * ngAnimate will handle the entire transition for us as well as the addition and removal of + * any changes of CSS classes between the elements: + * + * ```css + * .banner.ng-anchor { + * /* this animation will last for 1 second since there are + * two phases to the animation (an `in` and an `out` phase) */ + * transition:0.5s linear all; + * } + * ``` + * + * We also **must** include animations for the views that are being entered and removed + * (otherwise anchoring wouldn't be possible since the new view would be inserted right away). + * + * ```css + * .view-animation.ng-enter, .view-animation.ng-leave { + * transition:0.5s linear all; + * position:fixed; + * left:0; + * top:0; + * width:100%; + * } + * .view-animation.ng-enter { + * transform:translateX(100%); + * } + * .view-animation.ng-leave, + * .view-animation.ng-enter.ng-enter-active { + * transform:translateX(0%); + * } + * .view-animation.ng-leave.ng-leave-active { + * transform:translateX(-100%); + * } + * ``` + * + * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur: + * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away + * from its origin. Once that animation is over then the `in` stage occurs which animates the + * element to its destination. The reason why there are two animations is to give enough time + * for the enter animation on the new element to be ready. + * + * The example above sets up a transition for both the in and out phases, but we can also target the out or + * in phases directly via `ng-anchor-out` and `ng-anchor-in`. + * + * ```css + * .banner.ng-anchor-out { + * transition: 0.5s linear all; + * + * /* the scale will be applied during the out animation, + * but will be animated away when the in animation runs */ + * transform: scale(1.2); + * } + * + * .banner.ng-anchor-in { + * transition: 1s linear all; + * } + * ``` + * + * + * + * + * ### Anchoring Demo + * + + + Home +
+
+
+
+
+ + angular.module('anchoringExample', ['ngAnimate', 'ngRoute']) + .config(['$routeProvider', function($routeProvider) { + $routeProvider.when('/', { + templateUrl: 'home.html', + controller: 'HomeController as home' + }); + $routeProvider.when('/profile/:id', { + templateUrl: 'profile.html', + controller: 'ProfileController as profile' + }); + }]) + .run(['$rootScope', function($rootScope) { + $rootScope.records = [ + { id:1, title: "Miss Beulah Roob" }, + { id:2, title: "Trent Morissette" }, + { id:3, title: "Miss Ava Pouros" }, + { id:4, title: "Rod Pouros" }, + { id:5, title: "Abdul Rice" }, + { id:6, title: "Laurie Rutherford Sr." }, + { id:7, title: "Nakia McLaughlin" }, + { id:8, title: "Jordon Blanda DVM" }, + { id:9, title: "Rhoda Hand" }, + { id:10, title: "Alexandrea Sauer" } + ]; + }]) + .controller('HomeController', [function() { + //empty + }]) + .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) { + var index = parseInt($routeParams.id, 10); + var record = $rootScope.records[index - 1]; + + this.title = record.title; + this.id = record.id; + }]); + + +

Welcome to the home page

+

Please click on an element

+ + {{ record.title }} + +
+ +
+ {{ profile.title }} +
+
+ + .record { + display:block; + font-size:20px; + } + .profile { + background:black; + color:white; + font-size:100px; + } + .view-container { + position:relative; + } + .view-container > .view.ng-animate { + position:absolute; + top:0; + left:0; + width:100%; + min-height:500px; + } + .view.ng-enter, .view.ng-leave, + .record.ng-anchor { + transition:0.5s linear all; + } + .view.ng-enter { + transform:translateX(100%); + } + .view.ng-enter.ng-enter-active, .view.ng-leave { + transform:translateX(0%); + } + .view.ng-leave.ng-leave-active { + transform:translateX(-100%); + } + .record.ng-anchor-out { + background:red; + } + +
+ * + * ### How is the element transported? + * + * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting + * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element + * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The + * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match + * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied + * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class + * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element + * will become visible since the shim class will be removed. + * + * ### How is the morphing handled? + * + * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out + * what CSS classes differ between the starting element and the destination element. These different CSS classes + * will be added/removed on the anchor element and a transition will be applied (the transition that is provided + * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will + * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that + * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since + * the cloned element is placed inside of root element which is likely close to the body element). + * + * Note that if the root element is on the `` element then the cloned node will be placed inside of body. + * + * + * ## Using $animate in your directive code + * + * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application? + * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's + * imagine we have a greeting box that shows and hides itself when the data changes + * + * ```html + * Hi there + * ``` + * + * ```js + * ngModule.directive('greetingBox', ['$animate', function($animate) { + * return function(scope, element, attrs) { + * attrs.$observe('active', function(value) { + * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on'); + * }); + * }); + * }]); + * ``` + * + * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element + * in our HTML code then we can trigger a CSS or JS animation to happen. + * + * ```css + * /* normally we would create a CSS class to reference on the element */ + * greeting-box.on { transition:0.5s linear all; background:green; color:white; } + * ``` + * + * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's + * possible be sure to visit the {@link ng.$animate $animate service API page}. + * + * + * ### Preventing Collisions With Third Party Libraries + * + * Some third-party frameworks place animation duration defaults across many element or className + * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which + * is expecting actual animations on these elements and has to wait for their completion. + * + * You can prevent this unwanted behavior by using a prefix on all your animation classes: + * + * ```css + * /* prefixed with animate- */ + * .animate-fade-add.animate-fade-add-active { + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * You then configure `$animate` to enforce this prefix: + * + * ```js + * $animateProvider.classNameFilter(/animate-/); + * ``` + * + * This also may provide your application with a speed boost since only specific elements containing CSS class prefix + * will be evaluated for animation when any DOM changes occur in the application. + * + * ## Callbacks and Promises + * + * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger + * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has + * ended by chaining onto the returned promise that animation method returns. + * + * ```js + * // somewhere within the depths of the directive + * $animate.enter(element, parent).then(function() { + * //the animation has completed + * }); + * ``` + * + * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case + * anymore.) + * + * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering + * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view + * routing controller to hook into that: + * + * ```js + * ngModule.controller('HomePageController', ['$animate', function($animate) { + * $animate.on('enter', ngViewElement, function(element) { + * // the animation for this route has completed + * }]); + * }]) + * ``` + * + * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.) + */ + +/** + * @ngdoc service + * @name $animate + * @kind object + * + * @description + * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. + * + * Click here {@link ng.$animate to learn more about animations with `$animate`}. + */ +angular.module('ngAnimate', []) + .directive('ngAnimateSwap', ngAnimateSwapDirective) + + .directive('ngAnimateChildren', $$AnimateChildrenDirective) + .factory('$$rAFScheduler', $$rAFSchedulerFactory) + + .provider('$$animateQueue', $$AnimateQueueProvider) + .provider('$$animation', $$AnimationProvider) + + .provider('$animateCss', $AnimateCssProvider) + .provider('$$animateCssDriver', $$AnimateCssDriverProvider) + + .provider('$$animateJs', $$AnimateJsProvider) + .provider('$$animateJsDriver', $$AnimateJsDriverProvider); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-animate.min.js b/src/main/resources/static/lib/angular/angular-animate.min.js new file mode 100644 index 00000000..8e8e41df --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-animate.min.js @@ -0,0 +1,56 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(D,r,Va){'use strict';function ya(a,b,c){if(!a)throw Ka("areq",b||"?",c||"required");return a}function za(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;ba(a)&&(a=a.join(" "));ba(b)&&(b=b.join(" "));return a+" "+b}function La(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function X(a,b,c){var d="";a=ba(a)?a:a&&R(a)&&a.length?a.split(/\s+/):[];s(a,function(a,g){a&&0=a&&(a=e,e=0,b.push(t),t=[]);t.push(g.fn);g.children.forEach(function(a){e++; +c.push(a)});a--}t.length&&b.push(t);return b}(c)}var M=[],r=U(a);return function(u,A,v){function z(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function K(a){var b=[],c={};s(a,function(a,f){var d=G(a.element),h=0<=["enter","move"].indexOf(a.event),d=a.structural?z(d):[];if(d.length){var e=h?"to":"from";s(d,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]|| +{};c[b][e]={animationID:f,element:I(a)}})}else b.push(a)});var d={},h={};s(c,function(c,e){var l=c.from,t=c.to;if(l&&t){var g=a[l.animationID],E=a[t.animationID],k=l.animationID.toString();if(!h[k]){var z=h[k]={structural:!0,beforeStart:function(){g.beforeStart();E.beforeStart()},close:function(){g.close();E.close()},classes:J(g.classes,E.classes),from:g,to:E,anchors:[]};z.classes.length?b.push(z):(b.push(g),b.push(E))}h[k].anchors.push({out:l.element,"in":t.element})}else l=l?l.animationID:t.animationID, +t=l.toString(),d[t]||(d[t]=!0,b.push(a[l]))});return b}function J(a,b){a=a.split(" ");b=b.split(" ");for(var c=[],d=0;d=P&&b>=O&&(wa=!0,q())}function L(){function b(){if(!A){t(!1);s(m, +function(a){l.style[a[0]]=a[1]});z(a,f);e.addClass(a,ca);if(p.recalculateTimingStyles){ja=l.className+" "+da;ga=r(l,ja);F=v(l,ja,ga);$=F.maxDelay;n=Math.max($,0);O=F.maxDuration;if(0===O){q();return}p.hasTransitions=0B.expectedEndTime)?H.cancel(B.timer):g.push(q)}L&&(k=H(c,k,!1),g[0]={timer:k,expectedEndTime:d},g.push(q),a.data("$$animateCss",g));if(ea.length)a.on(ea.join(" "),E);f.to&&(f.cleanupStyles&&Ga(x,l,Object.keys(f.to)),Ba(a, +f))}}function c(){var b=a.data("$$animateCss");if(b){for(var d=1;dARIA](http://www.w3.org/TR/wai-aria/) + * attributes that convey state or semantic information about the application for users + * of assistive technologies, such as screen readers. + * + *
+ * + * ## Usage + * + * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following + * directives are supported: + * `ngModel`, `ngChecked`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, + * `ngDblClick`, and `ngMessages`. + * + * Below is a more detailed breakdown of the attributes handled by ngAria: + * + * | Directive | Supported Attributes | + * |---------------------------------------------|----------------------------------------------------------------------------------------| + * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles | + * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled | + * | {@link ng.directive:ngRequired ngRequired} | aria-required | + * | {@link ng.directive:ngChecked ngChecked} | aria-checked | + * | {@link ng.directive:ngValue ngValue} | aria-checked | + * | {@link ng.directive:ngShow ngShow} | aria-hidden | + * | {@link ng.directive:ngHide ngHide} | aria-hidden | + * | {@link ng.directive:ngDblclick ngDblclick} | tabindex | + * | {@link module:ngMessages ngMessages} | aria-live | + * | {@link ng.directive:ngClick ngClick} | tabindex, keypress event, button role | + * + * Find out more information about each directive by reading the + * {@link guide/accessibility ngAria Developer Guide}. + * + * ##Example + * Using ngDisabled with ngAria: + * ```html + * + * ``` + * Becomes: + * ```html + * + * ``` + * + * ##Disabling Attributes + * It's possible to disable individual attributes added by ngAria with the + * {@link ngAria.$ariaProvider#config config} method. For more details, see the + * {@link guide/accessibility Developer Guide}. + */ + /* global -ngAriaModule */ +var ngAriaModule = angular.module('ngAria', ['ng']). + provider('$aria', $AriaProvider); + +/** +* Internal Utilities +*/ +var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY']; + +var isNodeOneOf = function(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) { + return true; + } +}; +/** + * @ngdoc provider + * @name $ariaProvider + * + * @description + * + * Used for configuring the ARIA attributes injected and managed by ngAria. + * + * ```js + * angular.module('myApp', ['ngAria'], function config($ariaProvider) { + * $ariaProvider.config({ + * ariaValue: true, + * tabindex: false + * }); + * }); + *``` + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + * + */ +function $AriaProvider() { + var config = { + ariaHidden: true, + ariaChecked: true, + ariaDisabled: true, + ariaRequired: true, + ariaInvalid: true, + ariaValue: true, + tabindex: true, + bindKeypress: true, + bindRoleForClick: true + }; + + /** + * @ngdoc method + * @name $ariaProvider#config + * + * @param {object} config object to enable/disable specific ARIA attributes + * + * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags + * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags + * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags + * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags + * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags + * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags + * - **tabindex** – `{boolean}` – Enables/disables tabindex tags + * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `div` and + * `li` elements with ng-click + * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div` + * using ng-click, making them more accessible to users of assistive technologies + * + * @description + * Enables/disables various ARIA attributes + */ + this.config = function(newConfig) { + config = angular.extend(config, newConfig); + }; + + function watchExpr(attrName, ariaAttr, nodeBlackList, negate) { + return function(scope, elem, attr) { + var ariaCamelName = attr.$normalize(ariaAttr); + if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) { + scope.$watch(attr[attrName], function(boolVal) { + // ensure boolean value + boolVal = negate ? !boolVal : !!boolVal; + elem.attr(ariaAttr, boolVal); + }); + } + }; + } + /** + * @ngdoc service + * @name $aria + * + * @description + * @priority 200 + * + * The $aria service contains helper methods for applying common + * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives. + * + * ngAria injects common accessibility attributes that tell assistive technologies when HTML + * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria, + * let's review a code snippet from ngAria itself: + * + *```js + * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) { + * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); + * }]) + *``` + * Shown above, the ngAria module creates a directive with the same signature as the + * traditional `ng-disabled` directive. But this ngAria version is dedicated to + * solely managing accessibility attributes on custom elements. The internal `$aria` service is + * used to watch the boolean attribute `ngDisabled`. If it has not been explicitly set by the + * developer, `aria-disabled` is injected as an attribute with its value synchronized to the + * value in `ngDisabled`. + * + * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do + * anything to enable this feature. The `aria-disabled` attribute is automatically managed + * simply as a silent side-effect of using `ng-disabled` with the ngAria module. + * + * The full list of directives that interface with ngAria: + * * **ngModel** + * * **ngChecked** + * * **ngRequired** + * * **ngDisabled** + * * **ngValue** + * * **ngShow** + * * **ngHide** + * * **ngClick** + * * **ngDblclick** + * * **ngMessages** + * + * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each + * directive. + * + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + */ + this.$get = function() { + return { + config: function(key) { + return config[key]; + }, + $$watchExpr: watchExpr + }; + }; +} + + +ngAriaModule.directive('ngShow', ['$aria', function($aria) { + return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true); +}]) +.directive('ngHide', ['$aria', function($aria) { + return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false); +}]) +.directive('ngValue', ['$aria', function($aria) { + return $aria.$$watchExpr('ngValue', 'aria-checked', nodeBlackList, false); +}]) +.directive('ngChecked', ['$aria', function($aria) { + return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false); +}]) +.directive('ngRequired', ['$aria', function($aria) { + return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false); +}]) +.directive('ngModel', ['$aria', function($aria) { + + function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) { + return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)); + } + + function shouldAttachRole(role, elem) { + // if element does not have role attribute + // AND element type is equal to role (if custom element has a type equaling shape) <-- remove? + // AND element is not INPUT + return !elem.attr('role') && (elem.attr('type') === role) && (elem[0].nodeName !== 'INPUT'); + } + + function getShape(attr, elem) { + var type = attr.type, + role = attr.role; + + return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' : + ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' : + (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : ''; + } + + return { + restrict: 'A', + require: 'ngModel', + priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value + compile: function(elem, attr) { + var shape = getShape(attr, elem); + + return { + pre: function(scope, elem, attr, ngModel) { + if (shape === 'checkbox') { + //Use the input[checkbox] $isEmpty implementation for elements with checkbox roles + ngModel.$isEmpty = function(value) { + return value === false; + }; + } + }, + post: function(scope, elem, attr, ngModel) { + var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, false); + + function ngAriaWatchModelValue() { + return ngModel.$modelValue; + } + + function getRadioReaction(newVal) { + var boolVal = (attr.value == ngModel.$viewValue); + elem.attr('aria-checked', boolVal); + } + + function getCheckboxReaction() { + elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue)); + } + + switch (shape) { + case 'radio': + case 'checkbox': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', shape); + } + if (shouldAttachAttr('aria-checked', 'ariaChecked', elem, false)) { + scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? + getRadioReaction : getCheckboxReaction); + } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } + break; + case 'range': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', 'slider'); + } + if ($aria.config('ariaValue')) { + var needsAriaValuemin = !elem.attr('aria-valuemin') && + (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin')); + var needsAriaValuemax = !elem.attr('aria-valuemax') && + (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax')); + var needsAriaValuenow = !elem.attr('aria-valuenow'); + + if (needsAriaValuemin) { + attr.$observe('min', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemin', newVal); + }); + } + if (needsAriaValuemax) { + attr.$observe('max', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemax', newVal); + }); + } + if (needsAriaValuenow) { + scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { + elem.attr('aria-valuenow', newVal); + }); + } + } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } + break; + } + + if (!attr.hasOwnProperty('ngRequired') && ngModel.$validators.required + && shouldAttachAttr('aria-required', 'ariaRequired', elem, false)) { + // ngModel.$error.required is undefined on custom controls + attr.$observe('required', function() { + elem.attr('aria-required', !!attr['required']); + }); + } + + if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem, true)) { + scope.$watch(function ngAriaInvalidWatch() { + return ngModel.$invalid; + }, function ngAriaInvalidReaction(newVal) { + elem.attr('aria-invalid', !!newVal); + }); + } + } + }; + } + }; +}]) +.directive('ngDisabled', ['$aria', function($aria) { + return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); +}]) +.directive('ngMessages', function() { + return { + restrict: 'A', + require: '?ngMessages', + link: function(scope, elem, attr, ngMessages) { + if (!elem.attr('aria-live')) { + elem.attr('aria-live', 'assertive'); + } + } + }; +}) +.directive('ngClick',['$aria', '$parse', function($aria, $parse) { + return { + restrict: 'A', + compile: function(elem, attr) { + var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true); + return function(scope, elem, attr) { + + if (!isNodeOneOf(elem, nodeBlackList)) { + + if ($aria.config('bindRoleForClick') && !elem.attr('role')) { + elem.attr('role', 'button'); + } + + if ($aria.config('tabindex') && !elem.attr('tabindex')) { + elem.attr('tabindex', 0); + } + + if ($aria.config('bindKeypress') && !attr.ngKeypress) { + elem.on('keypress', function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode === 32 || keyCode === 13) { + scope.$apply(callback); + } + + function callback() { + fn(scope, { $event: event }); + } + }); + } + } + }; + } + }; +}]) +.directive('ngDblclick', ['$aria', function($aria) { + return function(scope, elem, attr) { + if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) { + elem.attr('tabindex', 0); + } + }; +}]); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-aria.min.js b/src/main/resources/static/lib/angular/angular-aria.min.js new file mode 100644 index 00000000..cf0fd742 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-aria.min.js @@ -0,0 +1,14 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(s,q,t){'use strict';var f="BUTTON A INPUT TEXTAREA SELECT DETAILS SUMMARY".split(" "),l=function(a,c){if(-1!==c.indexOf(a[0].nodeName))return!0};q.module("ngAria",["ng"]).provider("$aria",function(){function a(a,h,p,n){return function(d,e,b){var g=b.$normalize(h);!c[g]||l(e,p)||b[g]||d.$watch(b[a],function(b){b=n?!b:!!b;e.attr(h,b)})}}var c={ariaHidden:!0,ariaChecked:!0,ariaDisabled:!0,ariaRequired:!0,ariaInvalid:!0,ariaValue:!0,tabindex:!0,bindKeypress:!0,bindRoleForClick:!0};this.config= +function(a){c=q.extend(c,a)};this.$get=function(){return{config:function(a){return c[a]},$$watchExpr:a}}}).directive("ngShow",["$aria",function(a){return a.$$watchExpr("ngShow","aria-hidden",[],!0)}]).directive("ngHide",["$aria",function(a){return a.$$watchExpr("ngHide","aria-hidden",[],!1)}]).directive("ngValue",["$aria",function(a){return a.$$watchExpr("ngValue","aria-checked",f,!1)}]).directive("ngChecked",["$aria",function(a){return a.$$watchExpr("ngChecked","aria-checked",f,!1)}]).directive("ngRequired", +["$aria",function(a){return a.$$watchExpr("ngRequired","aria-required",f,!1)}]).directive("ngModel",["$aria",function(a){function c(c,n,d,e){return a.config(n)&&!d.attr(c)&&(e||!l(d,f))}function m(a,c){return!c.attr("role")&&c.attr("type")===a&&"INPUT"!==c[0].nodeName}function h(a,c){var d=a.type,e=a.role;return"checkbox"===(d||e)||"menuitemcheckbox"===e?"checkbox":"radio"===(d||e)||"menuitemradio"===e?"radio":"range"===d||"progressbar"===e||"slider"===e?"range":""}return{restrict:"A",require:"ngModel", +priority:200,compile:function(f,n){var d=h(n,f);return{pre:function(a,b,c,k){"checkbox"===d&&(k.$isEmpty=function(a){return!1===a})},post:function(e,b,g,k){function f(){return k.$modelValue}function h(a){b.attr("aria-checked",g.value==k.$viewValue)}function n(){b.attr("aria-checked",!k.$isEmpty(k.$viewValue))}var l=c("tabindex","tabindex",b,!1);switch(d){case "radio":case "checkbox":m(d,b)&&b.attr("role",d);c("aria-checked","ariaChecked",b,!1)&&e.$watch(f,"radio"===d?h:n);l&&b.attr("tabindex",0); +break;case "range":m(d,b)&&b.attr("role","slider");if(a.config("ariaValue")){var p=!b.attr("aria-valuemin")&&(g.hasOwnProperty("min")||g.hasOwnProperty("ngMin")),q=!b.attr("aria-valuemax")&&(g.hasOwnProperty("max")||g.hasOwnProperty("ngMax")),r=!b.attr("aria-valuenow");p&&g.$observe("min",function(a){b.attr("aria-valuemin",a)});q&&g.$observe("max",function(a){b.attr("aria-valuemax",a)});r&&e.$watch(f,function(a){b.attr("aria-valuenow",a)})}l&&b.attr("tabindex",0)}!g.hasOwnProperty("ngRequired")&& +k.$validators.required&&c("aria-required","ariaRequired",b,!1)&&g.$observe("required",function(){b.attr("aria-required",!!g.required)});c("aria-invalid","ariaInvalid",b,!0)&&e.$watch(function(){return k.$invalid},function(a){b.attr("aria-invalid",!!a)})}}}}}]).directive("ngDisabled",["$aria",function(a){return a.$$watchExpr("ngDisabled","aria-disabled",f,!1)}]).directive("ngMessages",function(){return{restrict:"A",require:"?ngMessages",link:function(a,c,f,h){c.attr("aria-live")||c.attr("aria-live", +"assertive")}}}).directive("ngClick",["$aria","$parse",function(a,c){return{restrict:"A",compile:function(m,h){var p=c(h.ngClick,null,!0);return function(c,d,e){if(!l(d,f)&&(a.config("bindRoleForClick")&&!d.attr("role")&&d.attr("role","button"),a.config("tabindex")&&!d.attr("tabindex")&&d.attr("tabindex",0),a.config("bindKeypress")&&!e.ngKeypress))d.on("keypress",function(a){function d(){p(c,{$event:a})}var e=a.which||a.keyCode;32!==e&&13!==e||c.$apply(d)})}}}}]).directive("ngDblclick",["$aria",function(a){return function(c, +m,h){!a.config("tabindex")||m.attr("tabindex")||l(m,f)||m.attr("tabindex",0)}}])})(window,window.angular); +//# sourceMappingURL=angular-aria.min.js.map diff --git a/src/main/resources/static/lib/angular/angular-aria.min.js.map b/src/main/resources/static/lib/angular/angular-aria.min.js.map new file mode 100644 index 00000000..b1db15b5 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-aria.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"angular-aria.min.js", +"lineCount":13, +"mappings":"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", +"sources":["angular-aria.js"], +"names":["window","angular","undefined","nodeBlackList","isNodeOneOf","elem","nodeTypeArray","indexOf","nodeName","module","provider","ngAriaModule","$AriaProvider","watchExpr","attrName","ariaAttr","negate","scope","attr","ariaCamelName","$normalize","config","$watch","boolVal","ariaHidden","ariaChecked","ariaDisabled","ariaRequired","ariaInvalid","ariaValue","tabindex","bindKeypress","bindRoleForClick","this.config","newConfig","extend","$get","this.$get","key","$$watchExpr","directive","$aria","shouldAttachAttr","normalizedAttr","allowBlacklistEls","shouldAttachRole","role","getShape","type","restrict","require","priority","compile","shape","pre","ngModel","$isEmpty","ngModel.$isEmpty","value","post","ngAriaWatchModelValue","$modelValue","getRadioReaction","newVal","$viewValue","getCheckboxReaction","needsTabIndex","needsAriaValuemin","hasOwnProperty","needsAriaValuemax","needsAriaValuenow","$observe","ngAriaValueMinReaction","ngAriaValueNowReaction","$validators","required","ngAriaInvalidWatch","$invalid","ngAriaInvalidReaction","link","ngMessages","$parse","fn","ngClick","ngKeypress","on","event","callback","$event","keyCode","which","$apply"] +} diff --git a/src/main/resources/static/lib/angular/angular-cookies.js b/src/main/resources/static/lib/angular/angular-cookies.js new file mode 100644 index 00000000..2157ef03 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-cookies.js @@ -0,0 +1,322 @@ +/** + * @license AngularJS v1.5.0 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/** + * @ngdoc module + * @name ngCookies + * @description + * + * # ngCookies + * + * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. + * + * + *
+ * + * See {@link ngCookies.$cookies `$cookies`} for usage. + */ + + +angular.module('ngCookies', ['ng']). + /** + * @ngdoc provider + * @name $cookiesProvider + * @description + * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. + * */ + provider('$cookies', [function $CookiesProvider() { + /** + * @ngdoc property + * @name $cookiesProvider#defaults + * @description + * + * Object containing default options to pass when setting cookies. + * + * The object may have following properties: + * + * - **path** - `{string}` - The cookie will be available only for this path and its + * sub-paths. By default, this is the URL that appears in your `` tag. + * - **domain** - `{string}` - The cookie will be available only for this domain and + * its sub-domains. For security reasons the user agent will not accept the cookie + * if the current domain is not a sub-domain of this domain or equal to it. + * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" + * or a Date object indicating the exact date/time this cookie will expire. + * - **secure** - `{boolean}` - If `true`, then the cookie will only be available through a + * secured connection. + * + * Note: By default, the address that appears in your `` tag will be used as the path. + * This is important so that cookies will be visible for all routes when html5mode is enabled. + * + **/ + var defaults = this.defaults = {}; + + function calcOptions(options) { + return options ? angular.extend({}, defaults, options) : defaults; + } + + /** + * @ngdoc service + * @name $cookies + * + * @description + * Provides read/write access to browser's cookies. + * + *
+ * Up until Angular 1.3, `$cookies` exposed properties that represented the + * current browser cookie values. In version 1.4, this behavior has changed, and + * `$cookies` now provides a standard api of getters, setters etc. + *
+ * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * @example + * + * ```js + * angular.module('cookiesExample', ['ngCookies']) + * .controller('ExampleController', ['$cookies', function($cookies) { + * // Retrieving a cookie + * var favoriteCookie = $cookies.get('myFavorite'); + * // Setting a cookie + * $cookies.put('myFavorite', 'oatmeal'); + * }]); + * ``` + */ + this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { + return { + /** + * @ngdoc method + * @name $cookies#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {string} Raw cookie value. + */ + get: function(key) { + return $$cookieReader()[key]; + }, + + /** + * @ngdoc method + * @name $cookies#getObject + * + * @description + * Returns the deserialized value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value. + */ + getObject: function(key) { + var value = this.get(key); + return value ? angular.fromJson(value) : value; + }, + + /** + * @ngdoc method + * @name $cookies#getAll + * + * @description + * Returns a key value object with all the cookies + * + * @returns {Object} All cookies + */ + getAll: function() { + return $$cookieReader(); + }, + + /** + * @ngdoc method + * @name $cookies#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {string} value Raw value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + put: function(key, value, options) { + $$cookieWriter(key, value, calcOptions(options)); + }, + + /** + * @ngdoc method + * @name $cookies#putObject + * + * @description + * Serializes and sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + putObject: function(key, value, options) { + this.put(key, angular.toJson(value), options); + }, + + /** + * @ngdoc method + * @name $cookies#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + remove: function(key, options) { + $$cookieWriter(key, undefined, calcOptions(options)); + } + }; + }]; + }]); + +angular.module('ngCookies'). +/** + * @ngdoc service + * @name $cookieStore + * @deprecated + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + *
+ * **Note:** The $cookieStore service is **deprecated**. + * Please use the {@link ngCookies.$cookies `$cookies`} service instead. + *
+ * + * @example + * + * ```js + * angular.module('cookieStoreExample', ['ngCookies']) + * .controller('ExampleController', ['$cookieStore', function($cookieStore) { + * // Put cookie + * $cookieStore.put('myFavorite','oatmeal'); + * // Get cookie + * var favoriteCookie = $cookieStore.get('myFavorite'); + * // Removing a cookie + * $cookieStore.remove('myFavorite'); + * }]); + * ``` + */ + factory('$cookieStore', ['$cookies', function($cookies) { + + return { + /** + * @ngdoc method + * @name $cookieStore#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. + */ + get: function(key) { + return $cookies.getObject(key); + }, + + /** + * @ngdoc method + * @name $cookieStore#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + */ + put: function(key, value) { + $cookies.putObject(key, value); + }, + + /** + * @ngdoc method + * @name $cookieStore#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + $cookies.remove(key); + } + }; + + }]); + +/** + * @name $$cookieWriter + * @requires $document + * + * @description + * This is a private service for writing cookies + * + * @param {string} name Cookie name + * @param {string=} value Cookie value (if undefined, cookie will be deleted) + * @param {Object=} options Object with options that need to be stored for the cookie. + */ +function $$CookieWriter($document, $log, $browser) { + var cookiePath = $browser.baseHref(); + var rawDocument = $document[0]; + + function buildCookieString(name, value, options) { + var path, expires; + options = options || {}; + expires = options.expires; + path = angular.isDefined(options.path) ? options.path : cookiePath; + if (angular.isUndefined(value)) { + expires = 'Thu, 01 Jan 1970 00:00:00 GMT'; + value = ''; + } + if (angular.isString(expires)) { + expires = new Date(expires); + } + + var str = encodeURIComponent(name) + '=' + encodeURIComponent(value); + str += path ? ';path=' + path : ''; + str += options.domain ? ';domain=' + options.domain : ''; + str += expires ? ';expires=' + expires.toUTCString() : ''; + str += options.secure ? ';secure' : ''; + + // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: + // - 300 cookies + // - 20 cookies per unique domain + // - 4096 bytes per cookie + var cookieLength = str.length + 1; + if (cookieLength > 4096) { + $log.warn("Cookie '" + name + + "' possibly not set or overflowed because it was too large (" + + cookieLength + " > 4096 bytes)!"); + } + + return str; + } + + return function(name, value, options) { + rawDocument.cookie = buildCookieString(name, value, options); + }; +} + +$$CookieWriter.$inject = ['$document', '$log', '$browser']; + +angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { + this.$get = $$CookieWriter; +}); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-cookies.min.js b/src/main/resources/static/lib/angular/angular-cookies.min.js new file mode 100644 index 00000000..d0f126a9 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-cookies.min.js @@ -0,0 +1,9 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(p,c,n){'use strict';function l(b,a,g){var d=g.baseHref(),k=b[0];return function(b,e,f){var g,h;f=f||{};h=f.expires;g=c.isDefined(f.path)?f.path:d;c.isUndefined(e)&&(h="Thu, 01 Jan 1970 00:00:00 GMT",e="");c.isString(h)&&(h=new Date(h));e=encodeURIComponent(b)+"="+encodeURIComponent(e);e=e+(g?";path="+g:"")+(f.domain?";domain="+f.domain:"");e+=h?";expires="+h.toUTCString():"";e+=f.secure?";secure":"";f=e.length+1;4096 4096 bytes)!");k.cookie=e}}c.module("ngCookies",["ng"]).provider("$cookies",[function(){var b=this.defaults={};this.$get=["$$cookieReader","$$cookieWriter",function(a,g){return{get:function(d){return a()[d]},getObject:function(d){return(d=this.get(d))?c.fromJson(d):d},getAll:function(){return a()},put:function(d,a,m){g(d,a,m?c.extend({},b,m):b)},putObject:function(d,b,a){this.put(d,c.toJson(b),a)},remove:function(a,k){g(a,n,k?c.extend({},b,k):b)}}}]}]);c.module("ngCookies").factory("$cookieStore", +["$cookies",function(b){return{get:function(a){return b.getObject(a)},put:function(a,c){b.putObject(a,c)},remove:function(a){b.remove(a)}}}]);l.$inject=["$document","$log","$browser"];c.module("ngCookies").provider("$$cookieWriter",function(){this.$get=l})})(window,window.angular); +//# sourceMappingURL=angular-cookies.min.js.map diff --git a/src/main/resources/static/lib/angular/angular-cookies.min.js.map b/src/main/resources/static/lib/angular/angular-cookies.min.js.map new file mode 100644 index 00000000..555b5103 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-cookies.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"angular-cookies.min.js", +"lineCount":8, +"mappings":"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", +"sources":["angular-cookies.js"], +"names":["window","angular","undefined","$$CookieWriter","$document","$log","$browser","cookiePath","baseHref","rawDocument","name","value","options","path","expires","isDefined","isUndefined","isString","Date","str","encodeURIComponent","domain","toUTCString","secure","cookieLength","length","warn","cookie","module","provider","$CookiesProvider","defaults","$get","$$cookieReader","$$cookieWriter","get","key","getObject","fromJson","getAll","put","extend","putObject","toJson","remove","factory","$cookies","$inject","$$CookieWriterProvider"] +} diff --git a/src/main/resources/static/lib/angular/angular-csp.css b/src/main/resources/static/lib/angular/angular-csp.css new file mode 100644 index 00000000..f3cd926c --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-csp.css @@ -0,0 +1,21 @@ +/* Include this file in your html if you are using the CSP mode. */ + +@charset "UTF-8"; + +[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], +.ng-cloak, .x-ng-cloak, +.ng-hide:not(.ng-hide-animate) { + display: none !important; +} + +ng\:form { + display: block; +} + +.ng-animate-shim { + visibility:hidden; +} + +.ng-anchor { + position:absolute; +} diff --git a/src/main/resources/static/lib/angular/angular-loader.js b/src/main/resources/static/lib/angular/angular-loader.js new file mode 100644 index 00000000..77948fd5 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-loader.js @@ -0,0 +1,484 @@ +/** + * @license AngularJS v1.5.0 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ + +(function() {'use strict'; + function isFunction(value) {return typeof value === 'function';}; + +/* global: toDebugString: true */ + +function serializeObject(obj) { + var seen = []; + + return JSON.stringify(obj, function(key, val) { + val = toJsonReplacer(key, val); + if (isObject(val)) { + + if (seen.indexOf(val) >= 0) return '...'; + + seen.push(val); + } + return val; + }); +} + +function toDebugString(obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (isUndefined(obj)) { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj); + } + return obj; +} + +/** + * @description + * + * This object provides a utility for producing rich Error messages within + * Angular. It can be called as follows: + * + * var exampleMinErr = minErr('example'); + * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); + * + * The above creates an instance of minErr in the example namespace. The + * resulting error will have a namespaced error code of example.one. The + * resulting error will replace {0} with the value of foo, and {1} with the + * value of bar. The object is not restricted in the number of arguments it can + * take. + * + * If fewer arguments are specified than necessary for interpolation, the extra + * interpolation markers will be preserved in the final string. + * + * Since data will be parsed statically during a build step, some restrictions + * are applied with respect to how minErr instances are created and called. + * Instances should have names of the form namespaceMinErr for a minErr created + * using minErr('namespace') . Error codes, namespaces and template strings + * should all be static strings, not variables or general expressions. + * + * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. + * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance + */ + +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { + var SKIP_INDEXES = 2; + + var templateArgs = arguments, + code = templateArgs[0], + message = '[' + (module ? module + ':' : '') + code + '] ', + template = templateArgs[1], + paramPrefix, i; + + message += template.replace(/\{\d+\}/g, function(match) { + var index = +match.slice(1, -1), + shiftedIndex = index + SKIP_INDEXES; + + if (shiftedIndex < templateArgs.length) { + return toDebugString(templateArgs[shiftedIndex]); + } + + return match; + }); + + message += '\nhttp://errors.angularjs.org/1.5.0/' + + (module ? module + '/' : '') + code; + + for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + + encodeURIComponent(toDebugString(templateArgs[i])); + } + + return new ErrorConstructor(message); + }; +} + +/** + * @ngdoc type + * @name angular.Module + * @module ng + * @description + * + * Interface for configuring angular {@link angular.module modules}. + */ + +function setupModuleLoader(window) { + + var $injectorMinErr = minErr('$injector'); + var ngMinErr = minErr('ng'); + + function ensure(obj, name, factory) { + return obj[name] || (obj[name] = factory()); + } + + var angular = ensure(window, 'angular', Object); + + // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap + angular.$$minErr = angular.$$minErr || minErr; + + return ensure(angular, 'module', function() { + /** @type {Object.} */ + var modules = {}; + + /** + * @ngdoc function + * @name angular.module + * @module ng + * @description + * + * The `angular.module` is a global place for creating, registering and retrieving Angular + * modules. + * All modules (angular core or 3rd party) that should be available to an application must be + * registered using this mechanism. + * + * Passing one argument retrieves an existing {@link angular.Module}, + * whereas passing more than one argument creates a new {@link angular.Module} + * + * + * # Module + * + * A module is a collection of services, directives, controllers, filters, and configuration information. + * `angular.module` is used to configure the {@link auto.$injector $injector}. + * + * ```js + * // Create a new module + * var myModule = angular.module('myModule', []); + * + * // register a new service + * myModule.value('appName', 'MyCoolApp'); + * + * // configure existing services inside initialization blocks. + * myModule.config(['$locationProvider', function($locationProvider) { + * // Configure existing providers + * $locationProvider.hashPrefix('!'); + * }]); + * ``` + * + * Then you can create an injector and load your modules like this: + * + * ```js + * var injector = angular.injector(['ng', 'myModule']) + * ``` + * + * However it's more likely that you'll just use + * {@link ng.directive:ngApp ngApp} or + * {@link angular.bootstrap} to simplify this process for you. + * + * @param {!string} name The name of the module to create or retrieve. + * @param {!Array.=} requires If specified then new module is being created. If + * unspecified then the module is being retrieved for further configuration. + * @param {Function=} configFn Optional configuration function for the module. Same as + * {@link angular.Module#config Module#config()}. + * @returns {angular.Module} new module with the {@link angular.Module} api. + */ + return function module(name, requires, configFn) { + var assertNotHasOwnProperty = function(name, context) { + if (name === 'hasOwnProperty') { + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); + } + }; + + assertNotHasOwnProperty(name, 'module'); + if (requires && modules.hasOwnProperty(name)) { + modules[name] = null; + } + return ensure(modules, name, function() { + if (!requires) { + throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + + "the module name or forgot to load it. If registering a module ensure that you " + + "specify the dependencies as the second argument.", name); + } + + /** @type {!Array.>} */ + var invokeQueue = []; + + /** @type {!Array.} */ + var configBlocks = []; + + /** @type {!Array.} */ + var runBlocks = []; + + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); + + /** @type {angular.Module} */ + var moduleInstance = { + // Private state + _invokeQueue: invokeQueue, + _configBlocks: configBlocks, + _runBlocks: runBlocks, + + /** + * @ngdoc property + * @name angular.Module#requires + * @module ng + * + * @description + * Holds the list of modules which the injector will load before the current module is + * loaded. + */ + requires: requires, + + /** + * @ngdoc property + * @name angular.Module#name + * @module ng + * + * @description + * Name of the module. + */ + name: name, + + + /** + * @ngdoc method + * @name angular.Module#provider + * @module ng + * @param {string} name service name + * @param {Function} providerType Construction function for creating new instance of the + * service. + * @description + * See {@link auto.$provide#provider $provide.provider()}. + */ + provider: invokeLaterAndSetModuleName('$provide', 'provider'), + + /** + * @ngdoc method + * @name angular.Module#factory + * @module ng + * @param {string} name service name + * @param {Function} providerFunction Function for creating new instance of the service. + * @description + * See {@link auto.$provide#factory $provide.factory()}. + */ + factory: invokeLaterAndSetModuleName('$provide', 'factory'), + + /** + * @ngdoc method + * @name angular.Module#service + * @module ng + * @param {string} name service name + * @param {Function} constructor A constructor function that will be instantiated. + * @description + * See {@link auto.$provide#service $provide.service()}. + */ + service: invokeLaterAndSetModuleName('$provide', 'service'), + + /** + * @ngdoc method + * @name angular.Module#value + * @module ng + * @param {string} name service name + * @param {*} object Service instance object. + * @description + * See {@link auto.$provide#value $provide.value()}. + */ + value: invokeLater('$provide', 'value'), + + /** + * @ngdoc method + * @name angular.Module#constant + * @module ng + * @param {string} name constant name + * @param {*} object Constant value. + * @description + * Because the constants are fixed, they get applied before other provide methods. + * See {@link auto.$provide#constant $provide.constant()}. + */ + constant: invokeLater('$provide', 'constant', 'unshift'), + + /** + * @ngdoc method + * @name angular.Module#decorator + * @module ng + * @param {string} The name of the service to decorate. + * @param {Function} This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. + * @description + * See {@link auto.$provide#decorator $provide.decorator()}. + */ + decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + + /** + * @ngdoc method + * @name angular.Module#animation + * @module ng + * @param {string} name animation name + * @param {Function} animationFactory Factory function for creating new instance of an + * animation. + * @description + * + * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. + * + * + * Defines an animation hook that can be later used with + * {@link $animate $animate} service and directives that use this service. + * + * ```js + * module.animation('.animation-name', function($inject1, $inject2) { + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) + * ``` + * + * See {@link ng.$animateProvider#register $animateProvider.register()} and + * {@link ngAnimate ngAnimate module} for more information. + */ + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#filter + * @module ng + * @param {string} name Filter name - this must be a valid angular expression identifier + * @param {Function} filterFactory Factory function for creating new instance of filter. + * @description + * See {@link ng.$filterProvider#register $filterProvider.register()}. + * + *
+ * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
+ */ + filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#controller + * @module ng + * @param {string|Object} name Controller name, or an object map of controllers where the + * keys are the names and the values are the constructors. + * @param {Function} constructor Controller constructor function. + * @description + * See {@link ng.$controllerProvider#register $controllerProvider.register()}. + */ + controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), + + /** + * @ngdoc method + * @name angular.Module#directive + * @module ng + * @param {string|Object} name Directive name, or an object map of directives where the + * keys are the names and the values are the factories. + * @param {Function} directiveFactory Factory function for creating new instance of + * directives. + * @description + * See {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), + + /** + * @ngdoc method + * @name angular.Module#component + * @module ng + * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}) + * + * @description + * See {@link ng.$compileProvider#component $compileProvider.component()}. + */ + component: invokeLaterAndSetModuleName('$compileProvider', 'component'), + + /** + * @ngdoc method + * @name angular.Module#config + * @module ng + * @param {Function} configFn Execute this function on module load. Useful for service + * configuration. + * @description + * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. + */ + config: config, + + /** + * @ngdoc method + * @name angular.Module#run + * @module ng + * @param {Function} initializationFn Execute this function after injector creation. + * Useful for application initialization. + * @description + * Use this method to register work which should be performed when the injector is done + * loading all modules. + */ + run: function(block) { + runBlocks.push(block); + return this; + } + }; + + if (configFn) { + config(configFn); + } + + return moduleInstance; + + /** + * @param {string} provider + * @param {string} method + * @param {String=} insertMethod + * @returns {angular.Module} + */ + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; + return function() { + queue[insertMethod || 'push']([provider, method, arguments]); + return moduleInstance; + }; + } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method) { + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + }; + } + }); + }; + }); + +} + +setupModuleLoader(window); +})(window); + +/** + * Closure compiler type information + * + * @typedef { { + * requires: !Array., + * invokeQueue: !Array.>, + * + * service: function(string, Function):angular.Module, + * factory: function(string, Function):angular.Module, + * value: function(string, *):angular.Module, + * + * filter: function(string, Function):angular.Module, + * + * init: function(Function):angular.Module + * } } + */ +angular.Module; + diff --git a/src/main/resources/static/lib/angular/angular-loader.min.js b/src/main/resources/static/lib/angular/angular-loader.min.js new file mode 100644 index 00000000..316ee26e --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-loader.min.js @@ -0,0 +1,10 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(){'use strict';function d(b){return function(){var a=arguments[0],e;e="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.5.0/"+(b?b+"/":"")+a;for(a=1;a= line.length) { + index -= line.length; + } else { + return { line: i + 1, column: index + 1 }; + } + } +} +var PARSE_CACHE_FOR_TEXT_LITERALS = Object.create(null); + +function parseTextLiteral(text) { + var cachedFn = PARSE_CACHE_FOR_TEXT_LITERALS[text]; + if (cachedFn != null) { + return cachedFn; + } + function parsedFn(context) { return text; } + parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { + var unwatch = scope['$watch'](noop, + function textLiteralWatcher() { + if (isFunction(listener)) { listener.call(null, text, text, scope); } + unwatch(); + }, + objectEquality); + return unwatch; + }; + PARSE_CACHE_FOR_TEXT_LITERALS[text] = parsedFn; + parsedFn['exp'] = text; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + parsedFn['expressions'] = []; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + return parsedFn; +} + +function subtractOffset(expressionFn, offset) { + if (offset === 0) { + return expressionFn; + } + function minusOffset(value) { + return (value == void 0) ? value : value - offset; + } + function parsedFn(context) { return minusOffset(expressionFn(context)); } + var unwatch; + parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { + unwatch = scope['$watch'](expressionFn, + function pluralExpressionWatchListener(newValue, oldValue) { + if (isFunction(listener)) { listener.call(null, minusOffset(newValue), minusOffset(oldValue), scope); } + }, + objectEquality); + return unwatch; + }; + return parsedFn; +} + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global isFunction: false */ +/* global noop: false */ + +/** + * @constructor + * @private + */ +function MessageSelectorBase(expressionFn, choices) { + var self = this; + this.expressionFn = expressionFn; + this.choices = choices; + if (choices["other"] === void 0) { + throw $interpolateMinErr('reqother', '“other” is a required option.'); + } + this.parsedFn = function(context) { return self.getResult(context); }; + this.parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { + return self.watchDelegate(scope, listener, objectEquality); + }; + this.parsedFn['exp'] = expressionFn['exp']; + this.parsedFn['expressions'] = expressionFn['expressions']; +} + +MessageSelectorBase.prototype.getMessageFn = function getMessageFn(value) { + return this.choices[this.categorizeValue(value)]; +}; + +MessageSelectorBase.prototype.getResult = function getResult(context) { + return this.getMessageFn(this.expressionFn(context))(context); +}; + +MessageSelectorBase.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { + var watchers = new MessageSelectorWatchers(this, scope, listener, objectEquality); + return function() { watchers.cancelWatch(); }; +}; + +/** + * @constructor + * @private + */ +function MessageSelectorWatchers(msgSelector, scope, listener, objectEquality) { + var self = this; + this.scope = scope; + this.msgSelector = msgSelector; + this.listener = listener; + this.objectEquality = objectEquality; + this.lastMessage = void 0; + this.messageFnWatcher = noop; + var expressionFnListener = function(newValue, oldValue) { return self.expressionFnListener(newValue, oldValue); }; + this.expressionFnWatcher = scope['$watch'](msgSelector.expressionFn, expressionFnListener, objectEquality); +} + +MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnListener(newValue, oldValue) { + var self = this; + this.messageFnWatcher(); + var messageFnListener = function(newMessage, oldMessage) { return self.messageFnListener(newMessage, oldMessage); }; + var messageFn = this.msgSelector.getMessageFn(newValue); + this.messageFnWatcher = this.scope['$watch'](messageFn, messageFnListener, this.objectEquality); +}; + +MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) { + if (isFunction(this.listener)) { + this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope); + } + this.lastMessage = newMessage; +}; + +MessageSelectorWatchers.prototype.cancelWatch = function cancelWatch() { + this.expressionFnWatcher(); + this.messageFnWatcher(); +}; + +/** + * @constructor + * @extends MessageSelectorBase + * @private + */ +function SelectMessage(expressionFn, choices) { + MessageSelectorBase.call(this, expressionFn, choices); +} + +function SelectMessageProto() {} +SelectMessageProto.prototype = MessageSelectorBase.prototype; + +SelectMessage.prototype = new SelectMessageProto(); +SelectMessage.prototype.categorizeValue = function categorizeSelectValue(value) { + return (this.choices[value] !== void 0) ? value : "other"; +}; + +/** + * @constructor + * @extends MessageSelectorBase + * @private + */ +function PluralMessage(expressionFn, choices, offset, pluralCat) { + MessageSelectorBase.call(this, expressionFn, choices); + this.offset = offset; + this.pluralCat = pluralCat; +} + +function PluralMessageProto() {} +PluralMessageProto.prototype = MessageSelectorBase.prototype; + +PluralMessage.prototype = new PluralMessageProto(); +PluralMessage.prototype.categorizeValue = function categorizePluralValue(value) { + if (isNaN(value)) { + return "other"; + } else if (this.choices[value] !== void 0) { + return value; + } else { + var category = this.pluralCat(value - this.offset); + return (this.choices[category] !== void 0) ? category : "other"; + } +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global isFunction: false */ +/* global parseTextLiteral: false */ + +/** + * @constructor + * @private + */ +function InterpolationParts(trustedContext, allOrNothing) { + this.trustedContext = trustedContext; + this.allOrNothing = allOrNothing; + this.textParts = []; + this.expressionFns = []; + this.expressionIndices = []; + this.partialText = ''; + this.concatParts = null; +} + +InterpolationParts.prototype.flushPartialText = function flushPartialText() { + if (this.partialText) { + if (this.concatParts == null) { + this.textParts.push(this.partialText); + } else { + this.textParts.push(this.concatParts.join('')); + this.concatParts = null; + } + this.partialText = ''; + } +}; + +InterpolationParts.prototype.addText = function addText(text) { + if (text.length) { + if (!this.partialText) { + this.partialText = text; + } else if (this.concatParts) { + this.concatParts.push(text); + } else { + this.concatParts = [this.partialText, text]; + } + } +}; + +InterpolationParts.prototype.addExpressionFn = function addExpressionFn(expressionFn) { + this.flushPartialText(); + this.expressionIndices.push(this.textParts.length); + this.expressionFns.push(expressionFn); + this.textParts.push(''); +}; + +InterpolationParts.prototype.getExpressionValues = function getExpressionValues(context) { + var expressionValues = new Array(this.expressionFns.length); + for (var i = 0; i < this.expressionFns.length; i++) { + expressionValues[i] = this.expressionFns[i](context); + } + return expressionValues; +}; + +InterpolationParts.prototype.getResult = function getResult(expressionValues) { + for (var i = 0; i < this.expressionIndices.length; i++) { + var expressionValue = expressionValues[i]; + if (this.allOrNothing && expressionValue === void 0) return; + this.textParts[this.expressionIndices[i]] = expressionValue; + } + return this.textParts.join(''); +}; + + +InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression, originalText) { + var self = this; + this.flushPartialText(); + if (mustHaveExpression && this.expressionFns.length === 0) { + return void 0; + } + if (this.textParts.length === 0) { + return parseTextLiteral(''); + } + if (this.trustedContext && this.textParts.length > 1) { + $interpolateMinErr['throwNoconcat'](originalText); + } + if (this.expressionFns.length === 0) { + if (this.textParts.length != 1) { this.errorInParseLogic(); } + return parseTextLiteral(this.textParts[0]); + } + var parsedFn = function(context) { + return self.getResult(self.getExpressionValues(context)); + }; + parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { + return self.watchDelegate(scope, listener, objectEquality); + }; + + parsedFn['exp'] = originalText; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + parsedFn['expressions'] = new Array(this.expressionFns.length); // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + for (var i = 0; i < this.expressionFns.length; i++) { + parsedFn['expressions'][i] = this.expressionFns[i]['exp']; + } + + return parsedFn; +}; + +InterpolationParts.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { + var watcher = new InterpolationPartsWatcher(this, scope, listener, objectEquality); + return function() { watcher.cancelWatch(); }; +}; + +function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEquality) { + this.interpolationParts = interpolationParts; + this.scope = scope; + this.previousResult = (void 0); + this.listener = listener; + var self = this; + this.expressionFnsWatcher = scope['$watchGroup'](interpolationParts.expressionFns, function(newExpressionValues, oldExpressionValues) { + self.watchListener(newExpressionValues, oldExpressionValues); + }); +} + +InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) { + var result = this.interpolationParts.getResult(newExpressionValues); + if (isFunction(this.listener)) { + this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope); + } + this.previousResult = result; +}; + +InterpolationPartsWatcher.prototype.cancelWatch = function cancelWatch() { + this.expressionFnsWatcher(); +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global indexToLineAndColumn: false */ +/* global InterpolationParts: false */ +/* global PluralMessage: false */ +/* global SelectMessage: false */ +/* global subtractOffset: false */ + +// The params src and dst are exactly one of two types: NestedParserState or MessageFormatParser. +// This function is fully optimized by V8. (inspect via IRHydra or --trace-deopt.) +// The idea behind writing it this way is to avoid repeating oneself. This is the ONE place where +// the parser state that is saved/restored when parsing nested mustaches is specified. +function copyNestedParserState(src, dst) { + dst.expressionFn = src.expressionFn; + dst.expressionMinusOffsetFn = src.expressionMinusOffsetFn; + dst.pluralOffset = src.pluralOffset; + dst.choices = src.choices; + dst.choiceKey = src.choiceKey; + dst.interpolationParts = src.interpolationParts; + dst.ruleChoiceKeyword = src.ruleChoiceKeyword; + dst.msgStartIndex = src.msgStartIndex; + dst.expressionStartIndex = src.expressionStartIndex; +} + +function NestedParserState(parser) { + copyNestedParserState(parser, this); +} + +/** + * @constructor + * @private + */ +function MessageFormatParser(text, startIndex, $parse, pluralCat, stringifier, + mustHaveExpression, trustedContext, allOrNothing) { + this.text = text; + this.index = startIndex || 0; + this.$parse = $parse; + this.pluralCat = pluralCat; + this.stringifier = stringifier; + this.mustHaveExpression = !!mustHaveExpression; + this.trustedContext = trustedContext; + this.allOrNothing = !!allOrNothing; + this.expressionFn = null; + this.expressionMinusOffsetFn = null; + this.pluralOffset = null; + this.choices = null; + this.choiceKey = null; + this.interpolationParts = null; + this.msgStartIndex = null; + this.nestedStateStack = []; + this.parsedFn = null; + this.rule = null; + this.ruleStack = null; + this.ruleChoiceKeyword = null; + this.interpNestLevel = null; + this.expressionStartIndex = null; + this.stringStartIndex = null; + this.stringQuote = null; + this.stringInterestsRe = null; + this.angularOperatorStack = null; + this.textPart = null; +} + +// preserve v8 optimization. +var EMPTY_STATE = new NestedParserState(new MessageFormatParser( + /* text= */ '', /* startIndex= */ 0, /* $parse= */ null, /* pluralCat= */ null, /* stringifier= */ null, + /* mustHaveExpression= */ false, /* trustedContext= */ null, /* allOrNothing */ false)); + +MessageFormatParser.prototype.pushState = function pushState() { + this.nestedStateStack.push(new NestedParserState(this)); + copyNestedParserState(EMPTY_STATE, this); +}; + +MessageFormatParser.prototype.popState = function popState() { + if (this.nestedStateStack.length === 0) { + this.errorInParseLogic(); + } + var previousState = this.nestedStateStack.pop(); + copyNestedParserState(previousState, this); +}; + +// Oh my JavaScript! Who knew you couldn't match a regex at a specific +// location in a string but will always search forward?! +// Apparently you'll be growing this ability via the sticky flag (y) in +// ES6. I'll just to work around you for now. +MessageFormatParser.prototype.matchRe = function matchRe(re, search) { + re.lastIndex = this.index; + var match = re.exec(this.text); + if (match != null && (search === true || (match.index == this.index))) { + this.index = re.lastIndex; + return match; + } + return null; +}; + +MessageFormatParser.prototype.searchRe = function searchRe(re) { + return this.matchRe(re, true); +}; + + +MessageFormatParser.prototype.consumeRe = function consumeRe(re) { + // Without the sticky flag, we can't use the .test() method to consume a + // match at the current index. Instead, we'll use the slower .exec() method + // and verify match.index. + return !!this.matchRe(re); +}; + +// Run through our grammar avoiding deeply nested function call chains. +MessageFormatParser.prototype.run = function run(initialRule) { + this.ruleStack = [initialRule]; + do { + this.rule = this.ruleStack.pop(); + while (this.rule) { + this.rule(); + } + this.assertRuleOrNull(this.rule); + } while (this.ruleStack.length > 0); +}; + +MessageFormatParser.prototype.errorInParseLogic = function errorInParseLogic() { + throw $interpolateMinErr('logicbug', + 'The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”', + this.text); +}; + +MessageFormatParser.prototype.assertRuleOrNull = function assertRuleOrNull(rule) { + if (rule === void 0) { + this.errorInParseLogic(); + } +}; + +var NEXT_WORD_RE = /\s*(\w+)\s*/g; +MessageFormatParser.prototype.errorExpecting = function errorExpecting() { + // What was wrong with the syntax? Unsupported type, missing comma, or something else? + var match = this.matchRe(NEXT_WORD_RE), position; + if (match == null) { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqarg', + 'Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”', + position.line, position.column, this.text); + } + var word = match[1]; + if (word == "select" || word == "plural") { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqcomma', + 'Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”', + word, position.line, position.column, this.text); + } else { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('unknarg', + 'Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”', + word, position.line, position.column, this.text); + } +}; + +var STRING_START_RE = /['"]/g; +MessageFormatParser.prototype.ruleString = function ruleString() { + var match = this.matchRe(STRING_START_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('wantstring', + 'Expected the beginning of a string at line {0}, column {1} in text “{2}”', + position.line, position.column, this.text); + } + this.startStringAtMatch(match); +}; + +MessageFormatParser.prototype.startStringAtMatch = function startStringAtMatch(match) { + this.stringStartIndex = match.index; + this.stringQuote = match[0]; + this.stringInterestsRe = this.stringQuote == "'" ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE; + this.rule = this.ruleInsideString; +}; + +var SQUOTED_STRING_INTEREST_RE = /\\(?:\\|'|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|'/g; +var DQUOTED_STRING_INTEREST_RE = /\\(?:\\|"|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|"/g; +MessageFormatParser.prototype.ruleInsideString = function ruleInsideString() { + var match = this.searchRe(this.stringInterestsRe); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.stringStartIndex); + throw $interpolateMinErr('untermstr', + 'The string beginning at line {0}, column {1} is unterminated in text “{2}”', + position.line, position.column, this.text); + } + var chars = match[0]; + if (match == this.stringQuote) { + this.rule = null; + } +}; + +var PLURAL_OR_SELECT_ARG_TYPE_RE = /\s*(plural|select)\s*,\s*/g; +MessageFormatParser.prototype.rulePluralOrSelect = function rulePluralOrSelect() { + var match = this.searchRe(PLURAL_OR_SELECT_ARG_TYPE_RE); + if (match == null) { + this.errorExpecting(); + } + var argType = match[1]; + switch (argType) { + case "plural": this.rule = this.rulePluralStyle; break; + case "select": this.rule = this.ruleSelectStyle; break; + default: this.errorInParseLogic(); + } +}; + +MessageFormatParser.prototype.rulePluralStyle = function rulePluralStyle() { + this.choices = Object.create(null); + this.ruleChoiceKeyword = this.rulePluralValueOrKeyword; + this.rule = this.rulePluralOffset; +}; + +MessageFormatParser.prototype.ruleSelectStyle = function ruleSelectStyle() { + this.choices = Object.create(null); + this.ruleChoiceKeyword = this.ruleSelectKeyword; + this.rule = this.ruleSelectKeyword; +}; + +var NUMBER_RE = /[0]|(?:[1-9][0-9]*)/g; +var PLURAL_OFFSET_RE = new RegExp("\\s*offset\\s*:\\s*(" + NUMBER_RE.source + ")", "g"); + +MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() { + var match = this.matchRe(PLURAL_OFFSET_RE); + this.pluralOffset = (match == null) ? 0 : parseInt(match[1], 10); + this.expressionMinusOffsetFn = subtractOffset(this.expressionFn, this.pluralOffset); + this.rule = this.rulePluralValueOrKeyword; +}; + +MessageFormatParser.prototype.assertChoiceKeyIsNew = function assertChoiceKeyIsNew(choiceKey, index) { + if (this.choices[choiceKey] !== void 0) { + var position = indexToLineAndColumn(this.text, index); + throw $interpolateMinErr('dupvalue', + 'The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”', + choiceKey, position.line, position.column, this.text); + } +}; + +var SELECT_KEYWORD = /\s*(\w+)/g; +MessageFormatParser.prototype.ruleSelectKeyword = function ruleSelectKeyword() { + var match = this.matchRe(SELECT_KEYWORD); + if (match == null) { + this.parsedFn = new SelectMessage(this.expressionFn, this.choices).parsedFn; + this.rule = null; + return; + } + this.choiceKey = match[1]; + this.assertChoiceKeyIsNew(this.choiceKey, match.index); + this.rule = this.ruleMessageText; +}; + +var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp("\\s*(?:(?:=(" + NUMBER_RE.source + "))|(\\w+))", "g"); +MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValueOrKeyword() { + var match = this.matchRe(EXPLICIT_VALUE_OR_KEYWORD_RE); + if (match == null) { + this.parsedFn = new PluralMessage(this.expressionFn, this.choices, this.pluralOffset, this.pluralCat).parsedFn; + this.rule = null; + return; + } + if (match[1] != null) { + this.choiceKey = parseInt(match[1], 10); + } else { + this.choiceKey = match[2]; + } + this.assertChoiceKeyIsNew(this.choiceKey, match.index); + this.rule = this.ruleMessageText; +}; + +var BRACE_OPEN_RE = /\s*{/g; +var BRACE_CLOSE_RE = /}/g; +MessageFormatParser.prototype.ruleMessageText = function ruleMessageText() { + if (!this.consumeRe(BRACE_OPEN_RE)) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqopenbrace', + 'The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”', + this.choiceKey, position.line, position.column, this.text); + } + this.msgStartIndex = this.index; + this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); + this.rule = this.ruleInInterpolationOrMessageText; +}; + +// Note: Since "\" is used as an escape character, don't allow it to be part of the +// startSymbol/endSymbol when I add the feature to allow them to be redefined. +var INTERP_OR_END_MESSAGE_RE = /\\.|{{|}/g; +var INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE = /\\.|{{|#|}/g; +var ESCAPE_OR_MUSTACHE_BEGIN_RE = /\\.|{{/g; +MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function advanceInInterpolationOrMessageText() { + var currentIndex = this.index, match, re; + if (this.ruleChoiceKeyword == null) { // interpolation + match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); + if (match == null) { // End of interpolation text. Nothing more to process. + this.textPart = this.text.substring(currentIndex); + this.index = this.text.length; + return null; + } + } else { + match = this.searchRe(this.ruleChoiceKeyword == this.rulePluralValueOrKeyword ? + INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE : INTERP_OR_END_MESSAGE_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.msgStartIndex); + throw $interpolateMinErr('reqendbrace', + 'The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”', + this.choiceKey, position.line, position.column, this.text); + } + } + // match is non-null. + var token = match[0]; + this.textPart = this.text.substring(currentIndex, match.index); + return token; +}; + +MessageFormatParser.prototype.ruleInInterpolationOrMessageText = function ruleInInterpolationOrMessageText() { + var currentIndex = this.index; + var token = this.advanceInInterpolationOrMessageText(); + if (token == null) { + // End of interpolation text. Nothing more to process. + this.index = this.text.length; + this.interpolationParts.addText(this.text.substring(currentIndex)); + this.rule = null; + return; + } + if (token[0] == "\\") { + // unescape next character and continue + this.interpolationParts.addText(this.textPart + token[1]); + return; + } + this.interpolationParts.addText(this.textPart); + if (token == "{{") { + this.pushState(); + this.ruleStack.push(this.ruleEndMustacheInInterpolationOrMessage); + this.rule = this.ruleEnteredMustache; + } else if (token == "}") { + this.choices[this.choiceKey] = this.interpolationParts.toParsedFn(/*mustHaveExpression=*/false, this.text); + this.rule = this.ruleChoiceKeyword; + } else if (token == "#") { + this.interpolationParts.addExpressionFn(this.expressionMinusOffsetFn); + } else { + this.errorInParseLogic(); + } +}; + +MessageFormatParser.prototype.ruleInterpolate = function ruleInterpolate() { + this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); + this.rule = this.ruleInInterpolation; +}; + +MessageFormatParser.prototype.ruleInInterpolation = function ruleInInterpolation() { + var currentIndex = this.index; + var match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); + if (match == null) { + // End of interpolation text. Nothing more to process. + this.index = this.text.length; + this.interpolationParts.addText(this.text.substring(currentIndex)); + this.parsedFn = this.interpolationParts.toParsedFn(this.mustHaveExpression, this.text); + this.rule = null; + return; + } + var token = match[0]; + if (token[0] == "\\") { + // unescape next character and continue + this.interpolationParts.addText(this.text.substring(currentIndex, match.index) + token[1]); + return; + } + this.interpolationParts.addText(this.text.substring(currentIndex, match.index)); + this.pushState(); + this.ruleStack.push(this.ruleInterpolationEndMustache); + this.rule = this.ruleEnteredMustache; +}; + +MessageFormatParser.prototype.ruleInterpolationEndMustache = function ruleInterpolationEndMustache() { + var expressionFn = this.parsedFn; + this.popState(); + this.interpolationParts.addExpressionFn(expressionFn); + this.rule = this.ruleInInterpolation; +}; + +MessageFormatParser.prototype.ruleEnteredMustache = function ruleEnteredMustache() { + this.parsedFn = null; + this.ruleStack.push(this.ruleEndMustache); + this.rule = this.ruleAngularExpression; +}; + +MessageFormatParser.prototype.ruleEndMustacheInInterpolationOrMessage = function ruleEndMustacheInInterpolationOrMessage() { + var expressionFn = this.parsedFn; + this.popState(); + this.interpolationParts.addExpressionFn(expressionFn); + this.rule = this.ruleInInterpolationOrMessageText; +}; + + + +var INTERP_END_RE = /\s*}}/g; +MessageFormatParser.prototype.ruleEndMustache = function ruleEndMustache() { + var match = this.matchRe(INTERP_END_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqendinterp', + 'Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”', + '}}', position.line, position.column, this.text); + } + if (this.parsedFn == null) { + // If we parsed a MessageFormat extension, (e.g. select/plural today, maybe more some other + // day), then the result *has* to be a string and those rules would have already set + // this.parsedFn. If there was no MessageFormat extension, then there is no requirement to + // stringify the result and parsedFn isn't set. We set it here. While we could have set it + // unconditionally when exiting the Angular expression, I intend for us to not just replace + // $interpolate, but also to replace $parse in a future version (so ng-bind can work), and in + // such a case we do not want to unnecessarily stringify something if it's not going to be used + // in a string context. + this.parsedFn = this.$parse(this.expressionFn, this.stringifier); + this.parsedFn['exp'] = this.expressionFn['exp']; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.parsedFn['expressions'] = this.expressionFn['expressions']; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + } + this.rule = null; +}; + +MessageFormatParser.prototype.ruleAngularExpression = function ruleAngularExpression() { + this.angularOperatorStack = []; + this.expressionStartIndex = this.index; + this.rule = this.ruleInAngularExpression; +}; + +function getEndOperator(opBegin) { + switch (opBegin) { + case "{": return "}"; + case "[": return "]"; + case "(": return ")"; + default: return null; + } +} + +function getBeginOperator(opEnd) { + switch (opEnd) { + case "}": return "{"; + case "]": return "["; + case ")": return "("; + default: return null; + } +} + +// TODO(chirayu): The interpolation endSymbol must also be accounted for. It +// just so happens that "}" is an operator so it's in the list below. But we +// should support any other type of start/end interpolation symbol. +var INTERESTING_OPERATORS_RE = /[[\]{}()'",]/g; +MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularExpression() { + var startIndex = this.index; + var match = this.searchRe(INTERESTING_OPERATORS_RE); + var position; + if (match == null) { + if (this.angularOperatorStack.length === 0) { + // This is the end of the Angular expression so this is actually a + // success. Note that when inside an interpolation, this means we even + // consumed the closing interpolation symbols if they were curlies. This + // is NOT an error at this point but will become an error further up the + // stack when the part that saw the opening curlies is unable to find the + // closing ones. + this.index = this.text.length; + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; + return; + } + var innermostOperator = this.angularOperatorStack[0]; + throw $interpolateMinErr('badexpr', + 'Unexpected end of Angular expression. Expecting operator “{0}” at the end of the text “{1}”', + this.getEndOperator(innermostOperator), this.text); + } + var operator = match[0]; + if (operator == "'" || operator == '"') { + this.ruleStack.push(this.ruleInAngularExpression); + this.startStringAtMatch(match); + return; + } + if (operator == ",") { + if (this.trustedContext) { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('unsafe', + 'Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”', + this.trustedContext, position.line, position.column, this.text); + } + // only the top level comma has relevance. + if (this.angularOperatorStack.length === 0) { + // todo: does this need to be trimmed? + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, match.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, match.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; + this.rule = this.rulePluralOrSelect; + } + return; + } + if (getEndOperator(operator) != null) { + this.angularOperatorStack.unshift(operator); + return; + } + var beginOperator = getBeginOperator(operator); + if (beginOperator == null) { + this.errorInParseLogic(); + } + if (this.angularOperatorStack.length > 0) { + if (beginOperator == this.angularOperatorStack[0]) { + this.angularOperatorStack.shift(); + return; + } + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('badexpr', + 'Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”', + operator, position.line, position.column, getEndOperator(this.angularOperatorStack[0]), this.text); + } + // We are trying to pop off the operator stack but there really isn't anything to pop off. + this.index = match.index; + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global MessageFormatParser: false */ +/* global stringify: false */ + +/** + * @ngdoc service + * @name $$messageFormat + * + * @description + * Angular internal service to recognize MessageFormat extensions in interpolation expressions. + * For more information, see: + * https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit + * + * ## Example + * + * + * + *
+ *
+ * {{recipients.length, plural, offset:1 + * =0 {{{sender.name}} gave no gifts (\#=#)} + * =1 {{{sender.name}} gave one gift to {{recipients[0].name}} (\#=#)} + * one {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)} + * other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)} + * }} + *
+ *
+ * + * + * function Person(name, gender) { + * this.name = name; + * this.gender = gender; + * } + * + * var alice = new Person("Alice", "female"), + * bob = new Person("Bob", "male"), + * charlie = new Person("Charlie", "male"), + * harry = new Person("Harry Potter", "male"); + * + * angular.module('msgFmtExample', ['ngMessageFormat']) + * .controller('AppController', ['$scope', function($scope) { + * $scope.recipients = [alice, bob, charlie]; + * $scope.sender = harry; + * $scope.decreaseRecipients = function() { + * --$scope.recipients.length; + * }; + * }]); + * + * + * + * describe('MessageFormat plural', function() { + * it('should pluralize initial values', function() { + * var messageElem = element(by.binding('recipients.length')), decreaseRecipientsBtn = element(by.id('decreaseRecipients')); + * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave one gift to Alice (#=0)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)'); + * }); + * }); + * + *
+ */ +var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat( + $parse, $locale, $sce, $exceptionHandler) { + + function getStringifier(trustedContext, allOrNothing, text) { + return function stringifier(value) { + try { + value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value); + return allOrNothing && (value === void 0) ? value : stringify(value); + } catch (err) { + $exceptionHandler($interpolateMinErr['interr'](text, err)); + } + }; + } + + function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + var stringifier = getStringifier(trustedContext, allOrNothing, text); + var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier, + mustHaveExpression, trustedContext, allOrNothing); + parser.run(parser.ruleInterpolate); + return parser.parsedFn; + } + + return { + 'interpolate': interpolate + }; +}]; + +var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) { + if ($interpolate['startSymbol']() != "{{" || $interpolate['endSymbol']() != "}}") { + throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.'); + } + var interpolate = $$messageFormat['interpolate']; + interpolate['startSymbol'] = $interpolate['startSymbol']; + interpolate['endSymbol'] = $interpolate['endSymbol']; + return interpolate; +}]; + + +/** + * @ngdoc module + * @name ngMessageFormat + * @packageName angular-message-format + * @description + */ +var module = window['angular']['module']('ngMessageFormat', ['ng']); +module['factory']('$$messageFormat', $$MessageFormatFactory); +module['config'](['$provide', function($provide) { + $provide['decorator']('$interpolate', $$interpolateDecorator); +}]); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-message-format.min.js b/src/main/resources/static/lib/angular/angular-message-format.min.js new file mode 100644 index 00000000..f85a5876 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-message-format.min.js @@ -0,0 +1,26 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(h){'use strict';function C(a){if(null==a)return"";switch(typeof a){case "string":return a;case "number":return""+a;default:return D(a)}}function f(a,b){for(var d=a.split(/\n/g),k=0;k=c.length)b-=c.length;else return{h:k+1,f:b+1}}}function t(a){function b(){return a}var d=u[a];if(null!=d)return d;b.$$watchDelegate=function(b,d,c){var e=b.$watch(v,function(){m(d)&&d.call(null,a,a,b);e()},c);return e};u[a]=b;b.exp=a;b.expressions=[];return b}function F(a,b){function d(a){return void 0== +a?a:a-b}function c(b){return d(a(b))}if(0===b)return a;var e;c.$$watchDelegate=function(b,c,k){return e=b.$watch(a,function(a,k){m(c)&&c.call(null,d(a),d(k),b)},k)};return c}function l(a,b){var d=this;this.b=a;this.e=b;if(void 0===b.other)throw e("reqother");this.d=function(a){return d.D(a)};this.d.$$watchDelegate=function(a,b,c){return d.P(a,b,c)};this.d.exp=a.exp;this.d.expressions=a.expressions}function n(a,b,d,c){var e=this;this.scope=b;this.oa=a;this.v=d;this.qa=c;this.U=void 0;this.K=v;this.ka= +b.$watch(a.b,function(a){return e.ja(a)},c)}function p(a,b){l.call(this,a,b)}function w(){}function q(a,b,d,c){l.call(this,a,b);this.offset=d;this.M=c}function x(){}function g(a,b){this.u=a;this.B=b;this.i=[];this.g=[];this.J=[];this.s="";this.q=null}function r(a,b,d){this.c=a;this.scope=b;this.W=void 0;this.v=d;var c=this;this.la=b.$watchGroup(a.g,function(a,b){c.Ea(a,b)})}function s(a,b){b.b=a.b;b.C=a.C;b.w=a.w;b.e=a.e;b.k=a.k;b.c=a.c;b.n=a.n;b.F=a.F;b.l=a.l}function y(a){s(a,this)}function c(a, +b,d,c,e,E,f,g){this.text=a;this.index=b||0;this.A=d;this.M=c;this.Da=e;this.pa=!!E;this.u=f;this.B=!!g;this.F=this.c=this.k=this.e=this.w=this.C=this.b=null;this.L=[];this.G=this.j=this.ca=this.O=this.da=this.l=this.n=this.o=this.a=this.d=null}function z(a){switch(a){case "{":return"}";case "[":return"]";case "(":return")";default:return null}}function G(a){switch(a){case "}":return"{";case "]":return"[";case ")":return"(";default:return null}}var e=h.angular.$interpolateMinErr,v=h.angular.noop,m= +h.angular.isFunction,D=h.angular.toJson,u=Object.create(null);l.prototype.T=function(a){return this.e[this.R(a)]};l.prototype.D=function(a){return this.T(this.b(a))(a)};l.prototype.P=function(a,b,d){var c=new n(this,a,b,d);return function(){c.I()}};n.prototype.ja=function(a){var b=this;this.K();a=this.oa.T(a);this.K=this.scope.$watch(a,function(a,c){return b.na(a,c)},this.qa)};n.prototype.na=function(a,b){m(this.v)&&this.v.call(null,a,a===b?a:this.U,this.scope);this.U=a};n.prototype.I=function(){this.ka(); +this.K()};w.prototype=l.prototype;p.prototype=new w;p.prototype.R=function(a){return void 0!==this.e[a]?a:"other"};x.prototype=l.prototype;q.prototype=new x;q.prototype.R=function(a){if(isNaN(a))return"other";if(void 0!==this.e[a])return a;a=this.M(a-this.offset);return void 0!==this.e[a]?a:"other"};g.prototype.S=function(){this.s&&(null==this.q?this.i.push(this.s):(this.i.push(this.q.join("")),this.q=null),this.s="")};g.prototype.p=function(a){a.length&&(this.s?this.q?this.q.push(a):this.q=[this.s, +a]:this.s=a)};g.prototype.H=function(a){this.S();this.J.push(this.i.length);this.g.push(a);this.i.push("")};g.prototype.ma=function(a){for(var b=Array(this.g.length),d=0;d + * + *
+ *
You did not enter a field
+ *
+ * Your email must be between 5 and 100 characters long + *
+ *
+ * + * ``` + * + * Now whatever key/value entries are present within the provided object (in this case `$error`) then + * the ngMessages directive will render the inner first ngMessage directive (depending if the key values + * match the attribute value present on each ngMessage directive). In other words, if your errors + * object contains the following data: + * + * ```javascript + * + * myField.$error = { minlength : true, required : true }; + * ``` + * + * Then the `required` message will be displayed first. When required is false then the `minlength` message + * will be displayed right after (since these messages are ordered this way in the template HTML code). + * The prioritization of each message is determined by what order they're present in the DOM. + * Therefore, instead of having custom JavaScript code determine the priority of what errors are + * present before others, the presentation of the errors are handled within the template. + * + * By default, ngMessages will only display one error at a time. However, if you wish to display all + * messages then the `ng-messages-multiple` attribute flag can be used on the element containing the + * ngMessages directive to make this happen. + * + * ```html + * + *
...
+ * + * + * ... + * ``` + * + * ## Reusing and Overriding Messages + * In addition to prioritization, ngMessages also allows for including messages from a remote or an inline + * template. This allows for generic collection of messages to be reused across multiple parts of an + * application. + * + * ```html + * + * + *
+ *
+ *
+ * ``` + * + * However, including generic messages may not be useful enough to match all input fields, therefore, + * `ngMessages` provides the ability to override messages defined in the remote template by redefining + * them within the directive container. + * + * ```html + * + * + * + *
+ * + * + *
+ * + *
You did not enter your email address
+ * + * + *
Your email address is invalid
+ * + * + *
+ *
+ *
+ * ``` + * + * In the example HTML code above the message that is set on required will override the corresponding + * required message defined within the remote template. Therefore, with particular input fields (such + * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied + * while more generic messages can be used to handle other, more general input errors. + * + * ## Dynamic Messaging + * ngMessages also supports using expressions to dynamically change key values. Using arrays and + * repeaters to list messages is also supported. This means that the code below will be able to + * fully adapt itself and display the appropriate message when any of the expression data changes: + * + * ```html + *
+ * + *
+ *
You did not enter your email address
+ *
+ * + *
{{ errorMessage.text }}
+ *
+ *
+ *
+ * ``` + * + * The `errorMessage.type` expression can be a string value or it can be an array so + * that multiple errors can be associated with a single error message: + * + * ```html + * + *
+ *
You did not enter your email address
+ *
+ * Your email must be between 5 and 100 characters long + *
+ *
+ * ``` + * + * Feel free to use other structural directives such as ng-if and ng-switch to further control + * what messages are active and when. Be careful, if you place ng-message on the same element + * as these structural directives, Angular may not be able to determine if a message is active + * or not. Therefore it is best to place the ng-message on a child element of the structural + * directive. + * + * ```html + *
+ *
+ *
Please enter something
+ *
+ *
+ * ``` + * + * ## Animations + * If the `ngAnimate` module is active within the application then the `ngMessages`, `ngMessage` and + * `ngMessageExp` directives will trigger animations whenever any messages are added and removed from + * the DOM by the `ngMessages` directive. + * + * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS + * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no + * messages present. Therefore, CSS transitions and keyframes as well as JavaScript animations can + * hook into the animations whenever these classes are added/removed. + * + * Let's say that our HTML code for our messages container looks like so: + * + * ```html + * + * ``` + * + * Then the CSS animation code for the message container looks like so: + * + * ```css + * .my-messages { + * transition:1s linear all; + * } + * .my-messages.ng-active { + * // messages are visible + * } + * .my-messages.ng-inactive { + * // messages are hidden + * } + * ``` + * + * Whenever an inner message is attached (becomes visible) or removed (becomes hidden) then the enter + * and leave animation is triggered for each particular element bound to the `ngMessage` directive. + * + * Therefore, the CSS code for the inner messages looks like so: + * + * ```css + * .some-message { + * transition:1s linear all; + * } + * + * .some-message.ng-enter {} + * .some-message.ng-enter.ng-enter-active {} + * + * .some-message.ng-leave {} + * .some-message.ng-leave.ng-leave-active {} + * ``` + * + * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. + */ +angular.module('ngMessages', []) + + /** + * @ngdoc directive + * @module ngMessages + * @name ngMessages + * @restrict AE + * + * @description + * `ngMessages` is a directive that is designed to show and hide messages based on the state + * of a key/value object that it listens on. The directive itself complements error message + * reporting with the `ngModel` $error object (which stores a key/value state of validation errors). + * + * `ngMessages` manages the state of internal messages within its container element. The internal + * messages use the `ngMessage` directive and will be inserted/removed from the page depending + * on if they're present within the key/value object. By default, only one message will be displayed + * at a time and this depends on the prioritization of the messages within the template. (This can + * be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.) + * + * A remote template can also be used to promote message reusability and messages can also be + * overridden. + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @usage + * ```html + * + * + * ... + * ... + * ... + * + * + * + * + * ... + * ... + * ... + * + * ``` + * + * @param {string} ngMessages an angular expression evaluating to a key/value object + * (this is typically the $error object on an ngModel instance). + * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true + * + * @example + * + * + *
+ * + *
myForm.myName.$error = {{ myForm.myName.$error | json }}
+ * + *
+ *
You did not enter a field
+ *
Your field is too short
+ *
Your field is too long
+ *
+ *
+ *
+ * + * angular.module('ngMessagesExample', ['ngMessages']); + * + *
+ */ + .directive('ngMessages', ['$animate', function($animate) { + var ACTIVE_CLASS = 'ng-active'; + var INACTIVE_CLASS = 'ng-inactive'; + + return { + require: 'ngMessages', + restrict: 'AE', + controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + var ctrl = this; + var latestKey = 0; + var nextAttachId = 0; + + this.getAttachId = function getAttachId() { return nextAttachId++; }; + + var messages = this.messages = {}; + var renderLater, cachedCollection; + + this.render = function(collection) { + collection = collection || {}; + + renderLater = false; + cachedCollection = collection; + + // this is true if the attribute is empty or if the attribute value is truthy + var multiple = isAttrTruthy($scope, $attrs.ngMessagesMultiple) || + isAttrTruthy($scope, $attrs.multiple); + + var unmatchedMessages = []; + var matchedKeys = {}; + var messageItem = ctrl.head; + var messageFound = false; + var totalMessages = 0; + + // we use != instead of !== to allow for both undefined and null values + while (messageItem != null) { + totalMessages++; + var messageCtrl = messageItem.message; + + var messageUsed = false; + if (!messageFound) { + forEach(collection, function(value, key) { + if (!messageUsed && truthy(value) && messageCtrl.test(key)) { + // this is to prevent the same error name from showing up twice + if (matchedKeys[key]) return; + matchedKeys[key] = true; + + messageUsed = true; + messageCtrl.attach(); + } + }); + } + + if (messageUsed) { + // unless we want to display multiple messages then we should + // set a flag here to avoid displaying the next message in the list + messageFound = !multiple; + } else { + unmatchedMessages.push(messageCtrl); + } + + messageItem = messageItem.next; + } + + forEach(unmatchedMessages, function(messageCtrl) { + messageCtrl.detach(); + }); + + unmatchedMessages.length !== totalMessages + ? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS) + : $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + }; + + $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); + + this.reRender = function() { + if (!renderLater) { + renderLater = true; + $scope.$evalAsync(function() { + if (renderLater) { + cachedCollection && ctrl.render(cachedCollection); + } + }); + } + }; + + this.register = function(comment, messageCtrl) { + var nextKey = latestKey.toString(); + messages[nextKey] = { + message: messageCtrl + }; + insertMessageNode($element[0], comment, nextKey); + comment.$$ngMessageNode = nextKey; + latestKey++; + + ctrl.reRender(); + }; + + this.deregister = function(comment) { + var key = comment.$$ngMessageNode; + delete comment.$$ngMessageNode; + removeMessageNode($element[0], comment, key); + delete messages[key]; + ctrl.reRender(); + }; + + function findPreviousMessage(parent, comment) { + var prevNode = comment; + var parentLookup = []; + while (prevNode && prevNode !== parent) { + var prevKey = prevNode.$$ngMessageNode; + if (prevKey && prevKey.length) { + return messages[prevKey]; + } + + // dive deeper into the DOM and examine its children for any ngMessage + // comments that may be in an element that appears deeper in the list + if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) == -1) { + parentLookup.push(prevNode); + prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; + } else { + prevNode = prevNode.previousSibling || prevNode.parentNode; + } + } + } + + function insertMessageNode(parent, comment, key) { + var messageNode = messages[key]; + if (!ctrl.head) { + ctrl.head = messageNode; + } else { + var match = findPreviousMessage(parent, comment); + if (match) { + messageNode.next = match.next; + match.next = messageNode; + } else { + messageNode.next = ctrl.head; + ctrl.head = messageNode; + } + } + } + + function removeMessageNode(parent, comment, key) { + var messageNode = messages[key]; + + var match = findPreviousMessage(parent, comment); + if (match) { + match.next = messageNode.next; + } else { + ctrl.head = messageNode.next; + } + } + }] + }; + + function isAttrTruthy(scope, attr) { + return (isString(attr) && attr.length === 0) || //empty attribute + truthy(scope.$eval(attr)); + } + + function truthy(val) { + return isString(val) ? val.length : !!val; + } + }]) + + /** + * @ngdoc directive + * @name ngMessagesInclude + * @restrict AE + * @scope + * + * @description + * `ngMessagesInclude` is a directive with the purpose to import existing ngMessage template + * code from a remote template and place the downloaded template code into the exact spot + * that the ngMessagesInclude directive is placed within the ngMessages container. This allows + * for a series of pre-defined messages to be reused and also allows for the developer to + * determine what messages are overridden due to the placement of the ngMessagesInclude directive. + * + * @usage + * ```html + * + * + * ... + * + * + * + * + * ... + * + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {string} ngMessagesInclude|src a string value corresponding to the remote template. + */ + .directive('ngMessagesInclude', + ['$templateRequest', '$document', '$compile', function($templateRequest, $document, $compile) { + + return { + restrict: 'AE', + require: '^^ngMessages', // we only require this for validation sake + link: function($scope, element, attrs) { + var src = attrs.ngMessagesInclude || attrs.src; + $templateRequest(src).then(function(html) { + $compile(html)($scope, function(contents) { + element.after(contents); + + // the anchor is placed for debugging purposes + var anchor = jqLite($document[0].createComment(' ngMessagesInclude: ' + src + ' ')); + element.after(anchor); + + // we don't want to pollute the DOM anymore by keeping an empty directive element + element.remove(); + }); + }); + } + }; + }]) + + /** + * @ngdoc directive + * @name ngMessage + * @restrict AE + * @scope + * + * @description + * `ngMessage` is a directive with the purpose to show and hide a particular message. + * For `ngMessage` to operate, a parent `ngMessages` directive on a parent DOM element + * must be situated since it determines which messages are visible based on the state + * of the provided key/value map that `ngMessages` listens on. + * + * More information about using `ngMessage` can be found in the + * {@link module:ngMessages `ngMessages` module documentation}. + * + * @usage + * ```html + * + * + * ... + * ... + * + * + * + * + * ... + * ... + * + * ``` + * + * @param {expression} ngMessage|when a string value corresponding to the message key. + */ + .directive('ngMessage', ngMessageDirectiveFactory()) + + + /** + * @ngdoc directive + * @name ngMessageExp + * @restrict AE + * @priority 1 + * @scope + * + * @description + * `ngMessageExp` is a directive with the purpose to show and hide a particular message. + * For `ngMessageExp` to operate, a parent `ngMessages` directive on a parent DOM element + * must be situated since it determines which messages are visible based on the state + * of the provided key/value map that `ngMessages` listens on. + * + * @usage + * ```html + * + * + * ... + * + * + * + * + * ... + * + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. + */ + .directive('ngMessageExp', ngMessageDirectiveFactory()); + +function ngMessageDirectiveFactory() { + return ['$animate', function($animate) { + return { + restrict: 'AE', + transclude: 'element', + priority: 1, // must run before ngBind, otherwise the text is set on the comment + terminal: true, + require: '^^ngMessages', + link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { + var commentNode = element[0]; + + var records; + var staticExp = attrs.ngMessage || attrs.when; + var dynamicExp = attrs.ngMessageExp || attrs.whenExp; + var assignRecords = function(items) { + records = items + ? (isArray(items) + ? items + : items.split(/[\s,]+/)) + : null; + ngMessagesCtrl.reRender(); + }; + + if (dynamicExp) { + assignRecords(scope.$eval(dynamicExp)); + scope.$watchCollection(dynamicExp, assignRecords); + } else { + assignRecords(staticExp); + } + + var currentElement, messageCtrl; + ngMessagesCtrl.register(commentNode, messageCtrl = { + test: function(name) { + return contains(records, name); + }, + attach: function() { + if (!currentElement) { + $transclude(scope, function(elm) { + $animate.enter(elm, null, element); + currentElement = elm; + + // Each time we attach this node to a message we get a new id that we can match + // when we are destroying the node later. + var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId(); + + // in the event that the parent element is destroyed + // by any other structural directive then it's time + // to deregister the message from the controller + currentElement.on('$destroy', function() { + if (currentElement && currentElement.$$attachId === $$attachId) { + ngMessagesCtrl.deregister(commentNode); + messageCtrl.detach(); + } + }); + }); + } + }, + detach: function() { + if (currentElement) { + var elm = currentElement; + currentElement = null; + $animate.leave(elm); + } + } + }); + } + }; + }]; + + function contains(collection, key) { + if (collection) { + return isArray(collection) + ? collection.indexOf(key) >= 0 + : collection.hasOwnProperty(key); + } + } +} + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-messages.min.js b/src/main/resources/static/lib/angular/angular-messages.min.js new file mode 100644 index 00000000..19f59cc8 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-messages.min.js @@ -0,0 +1,12 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(A,d,B){'use strict';function l(){return["$animate",function(v){return{restrict:"AE",transclude:"element",priority:1,terminal:!0,require:"^^ngMessages",link:function(n,r,a,b,m){var k=r[0],f,p=a.ngMessage||a.when;a=a.ngMessageExp||a.whenExp;var d=function(a){f=a?w(a)?a:a.split(/[\s,]+/):null;b.reRender()};a?(d(n.$eval(a)),n.$watchCollection(a,d)):d(p);var e,q;b.register(k,q={test:function(a){var g=f;a=g?w(g)?0<=g.indexOf(a):g.hasOwnProperty(a):void 0;return a},attach:function(){e||m(n,function(a){v.enter(a, +null,r);e=a;var g=e.$$attachId=b.getAttachId();e.on("$destroy",function(){e&&e.$$attachId===g&&(b.deregister(k),q.detach())})})},detach:function(){if(e){var a=e;e=null;v.leave(a)}}})}}}]}var w=d.isArray,x=d.forEach,y=d.isString,z=d.element;d.module("ngMessages",[]).directive("ngMessages",["$animate",function(d){function n(a,b){return y(b)&&0===b.length||r(a.$eval(b))}function r(a){return y(a)?a.length:!!a}return{require:"ngMessages",restrict:"AE",controller:["$element","$scope","$attrs",function(a, +b,m){function k(a,b){for(var c=b,f=[];c&&c!==a;){var h=c.$$ngMessageNode;if(h&&h.length)return e[h];c.childNodes.length&&-1==f.indexOf(c)?(f.push(c),c=c.childNodes[c.childNodes.length-1]):c=c.previousSibling||c.parentNode}}var f=this,p=0,l=0;this.getAttachId=function(){return l++};var e=this.messages={},q,s;this.render=function(g){g=g||{};q=!1;s=g;for(var e=n(b,m.ngMessagesMultiple)||n(b,m.multiple),c=[],k={},h=f.head,p=!1,l=0;null!=h;){l++;var t=h.message,u=!1;p||x(g,function(a,c){!u&&r(a)&&t.test(c)&& +!k[c]&&(u=k[c]=!0,t.attach())});u?p=!e:c.push(t);h=h.next}x(c,function(a){a.detach()});c.length!==l?d.setClass(a,"ng-active","ng-inactive"):d.setClass(a,"ng-inactive","ng-active")};b.$watchCollection(m.ngMessages||m["for"],f.render);this.reRender=function(){q||(q=!0,b.$evalAsync(function(){q&&s&&f.render(s)}))};this.register=function(g,b){var c=p.toString();e[c]={message:b};var d=a[0],h=e[c];f.head?(d=k(d,g))?(h.next=d.next,d.next=h):(h.next=f.head,f.head=h):f.head=h;g.$$ngMessageNode=c;p++;f.reRender()}; +this.deregister=function(b){var d=b.$$ngMessageNode;delete b.$$ngMessageNode;var c=e[d];(b=k(a[0],b))?b.next=c.next:f.head=c.next;delete e[d];f.reRender()}}]}}]).directive("ngMessagesInclude",["$templateRequest","$document","$compile",function(d,n,l){return{restrict:"AE",require:"^^ngMessages",link:function(a,b,m){var k=m.ngMessagesInclude||m.src;d(k).then(function(d){l(d)(a,function(a){b.after(a);a=z(n[0].createComment(" ngMessagesInclude: "+k+" "));b.after(a);b.remove()})})}}}]).directive("ngMessage", +l()).directive("ngMessageExp",l())})(window,window.angular); +//# sourceMappingURL=angular-messages.min.js.map diff --git a/src/main/resources/static/lib/angular/angular-messages.min.js.map b/src/main/resources/static/lib/angular/angular-messages.min.js.map new file mode 100644 index 00000000..0cb2a943 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-messages.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"angular-messages.min.js", +"lineCount":11, +"mappings":"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", +"sources":["angular-messages.js"], +"names":["window","angular","undefined","ngMessageDirectiveFactory","$animate","restrict","transclude","priority","terminal","require","link","scope","element","attrs","ngMessagesCtrl","$transclude","commentNode","records","staticExp","ngMessage","when","dynamicExp","ngMessageExp","whenExp","assignRecords","items","isArray","split","reRender","$eval","$watchCollection","currentElement","messageCtrl","register","test","name","collection","indexOf","hasOwnProperty","attach","elm","enter","$$attachId","getAttachId","on","deregister","detach","leave","forEach","isString","jqLite","module","directive","isAttrTruthy","attr","length","truthy","val","controller","$element","$scope","$attrs","findPreviousMessage","parent","comment","prevNode","parentLookup","prevKey","$$ngMessageNode","messages","childNodes","push","previousSibling","parentNode","ctrl","latestKey","nextAttachId","this.getAttachId","renderLater","cachedCollection","render","this.render","multiple","ngMessagesMultiple","unmatchedMessages","matchedKeys","messageItem","head","messageFound","totalMessages","message","messageUsed","value","key","next","setClass","ACTIVE_CLASS","INACTIVE_CLASS","ngMessages","this.reRender","$evalAsync","this.register","nextKey","toString","messageNode","match","this.deregister","$templateRequest","$document","$compile","src","ngMessagesInclude","then","html","contents","after","anchor","createComment","remove"] +} diff --git a/src/main/resources/static/lib/angular/angular-mocks.js b/src/main/resources/static/lib/angular/angular-mocks.js new file mode 100644 index 00000000..34d36087 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-mocks.js @@ -0,0 +1,2842 @@ +/** + * @license AngularJS v1.5.0 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) { + +'use strict'; + +/** + * @ngdoc object + * @name angular.mock + * @description + * + * Namespace from 'angular-mocks.js' which contains testing related code. + */ +angular.mock = {}; + +/** + * ! This is a private undocumented service ! + * + * @name $browser + * + * @description + * This service is a mock implementation of {@link ng.$browser}. It provides fake + * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, + * cookies, etc... + * + * The api of this service is the same as that of the real {@link ng.$browser $browser}, except + * that there are several helper methods available which can be used in tests. + */ +angular.mock.$BrowserProvider = function() { + this.$get = function() { + return new angular.mock.$Browser(); + }; +}; + +angular.mock.$Browser = function() { + var self = this; + + this.isMock = true; + self.$$url = "http://server/"; + self.$$lastUrl = self.$$url; // used by url polling fn + self.pollFns = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = angular.noop; + self.$$incOutstandingRequestCount = angular.noop; + + + // register url polling fn + + self.onUrlChange = function(listener) { + self.pollFns.push( + function() { + if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { + self.$$lastUrl = self.$$url; + self.$$lastState = self.$$state; + listener(self.$$url, self.$$state); + } + } + ); + + return listener; + }; + + self.$$applicationDestroyed = angular.noop; + self.$$checkUrlChange = angular.noop; + + self.deferredFns = []; + self.deferredNextId = 0; + + self.defer = function(fn, delay) { + delay = delay || 0; + self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); + self.deferredFns.sort(function(a, b) { return a.time - b.time;}); + return self.deferredNextId++; + }; + + + /** + * @name $browser#defer.now + * + * @description + * Current milliseconds mock time. + */ + self.defer.now = 0; + + + self.defer.cancel = function(deferId) { + var fnIndex; + + angular.forEach(self.deferredFns, function(fn, index) { + if (fn.id === deferId) fnIndex = index; + }); + + if (angular.isDefined(fnIndex)) { + self.deferredFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + + /** + * @name $browser#defer.flush + * + * @description + * Flushes all pending requests and executes the defer callbacks. + * + * @param {number=} number of milliseconds to flush. See {@link #defer.now} + */ + self.defer.flush = function(delay) { + if (angular.isDefined(delay)) { + self.defer.now += delay; + } else { + if (self.deferredFns.length) { + self.defer.now = self.deferredFns[self.deferredFns.length - 1].time; + } else { + throw new Error('No deferred tasks to be flushed'); + } + } + + while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { + self.deferredFns.shift().fn(); + } + }; + + self.$$baseHref = '/'; + self.baseHref = function() { + return this.$$baseHref; + }; +}; +angular.mock.$Browser.prototype = { + +/** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ + poll: function poll() { + angular.forEach(this.pollFns, function(pollFn) { + pollFn(); + }); + }, + + url: function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } + if (url) { + this.$$url = url; + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); + return this; + } + + return this.$$url; + }, + + state: function() { + return this.$$state; + }, + + notifyWhenNoOutstandingRequests: function(fn) { + fn(); + } +}; + + +/** + * @ngdoc provider + * @name $exceptionHandlerProvider + * + * @description + * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors + * passed to the `$exceptionHandler`. + */ + +/** + * @ngdoc service + * @name $exceptionHandler + * + * @description + * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed + * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * information. + * + * + * ```js + * describe('$exceptionHandlerProvider', function() { + * + * it('should capture log messages and exceptions', function() { + * + * module(function($exceptionHandlerProvider) { + * $exceptionHandlerProvider.mode('log'); + * }); + * + * inject(function($log, $exceptionHandler, $timeout) { + * $timeout(function() { $log.log(1); }); + * $timeout(function() { $log.log(2); throw 'banana peel'; }); + * $timeout(function() { $log.log(3); }); + * expect($exceptionHandler.errors).toEqual([]); + * expect($log.assertEmpty()); + * $timeout.flush(); + * expect($exceptionHandler.errors).toEqual(['banana peel']); + * expect($log.log.logs).toEqual([[1], [2], [3]]); + * }); + * }); + * }); + * ``` + */ + +angular.mock.$ExceptionHandlerProvider = function() { + var handler; + + /** + * @ngdoc method + * @name $exceptionHandlerProvider#mode + * + * @description + * Sets the logging mode. + * + * @param {string} mode Mode of operation, defaults to `rethrow`. + * + * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` + * mode stores an array of errors in `$exceptionHandler.errors`, to allow later + * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and + * {@link ngMock.$log#reset reset()} + * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there + * is a bug in the application or test, so this mock will make these tests fail. + * For any implementations that expect exceptions to be thrown, the `rethrow` mode + * will also maintain a log of thrown errors. + */ + this.mode = function(mode) { + + switch (mode) { + case 'log': + case 'rethrow': + var errors = []; + handler = function(e) { + if (arguments.length == 1) { + errors.push(e); + } else { + errors.push([].slice.call(arguments, 0)); + } + if (mode === "rethrow") { + throw e; + } + }; + handler.errors = errors; + break; + default: + throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); + } + }; + + this.$get = function() { + return handler; + }; + + this.mode('rethrow'); +}; + + +/** + * @ngdoc service + * @name $log + * + * @description + * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays + * (one array per logging level). These arrays are exposed as `logs` property of each of the + * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. + * + */ +angular.mock.$LogProvider = function() { + var debug = true; + + function concat(array1, array2, index) { + return array1.concat(Array.prototype.slice.call(array2, index)); + } + + this.debugEnabled = function(flag) { + if (angular.isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = function() { + var $log = { + log: function() { $log.log.logs.push(concat([], arguments, 0)); }, + warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, + info: function() { $log.info.logs.push(concat([], arguments, 0)); }, + error: function() { $log.error.logs.push(concat([], arguments, 0)); }, + debug: function() { + if (debug) { + $log.debug.logs.push(concat([], arguments, 0)); + } + } + }; + + /** + * @ngdoc method + * @name $log#reset + * + * @description + * Reset all of the logging arrays to empty. + */ + $log.reset = function() { + /** + * @ngdoc property + * @name $log#log.logs + * + * @description + * Array of messages logged using {@link ng.$log#log `log()`}. + * + * @example + * ```js + * $log.log('Some Log'); + * var first = $log.log.logs.unshift(); + * ``` + */ + $log.log.logs = []; + /** + * @ngdoc property + * @name $log#info.logs + * + * @description + * Array of messages logged using {@link ng.$log#info `info()`}. + * + * @example + * ```js + * $log.info('Some Info'); + * var first = $log.info.logs.unshift(); + * ``` + */ + $log.info.logs = []; + /** + * @ngdoc property + * @name $log#warn.logs + * + * @description + * Array of messages logged using {@link ng.$log#warn `warn()`}. + * + * @example + * ```js + * $log.warn('Some Warning'); + * var first = $log.warn.logs.unshift(); + * ``` + */ + $log.warn.logs = []; + /** + * @ngdoc property + * @name $log#error.logs + * + * @description + * Array of messages logged using {@link ng.$log#error `error()`}. + * + * @example + * ```js + * $log.error('Some Error'); + * var first = $log.error.logs.unshift(); + * ``` + */ + $log.error.logs = []; + /** + * @ngdoc property + * @name $log#debug.logs + * + * @description + * Array of messages logged using {@link ng.$log#debug `debug()`}. + * + * @example + * ```js + * $log.debug('Some Error'); + * var first = $log.debug.logs.unshift(); + * ``` + */ + $log.debug.logs = []; + }; + + /** + * @ngdoc method + * @name $log#assertEmpty + * + * @description + * Assert that all of the logging methods have no logged messages. If any messages are present, + * an exception is thrown. + */ + $log.assertEmpty = function() { + var errors = []; + angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { + angular.forEach($log[logLevel].logs, function(log) { + angular.forEach(log, function(logItem) { + errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + + (logItem.stack || '')); + }); + }); + }); + if (errors.length) { + errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " + + "an expected log message was not checked and removed:"); + errors.push(''); + throw new Error(errors.join('\n---------\n')); + } + }; + + $log.reset(); + return $log; + }; +}; + + +/** + * @ngdoc service + * @name $interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. + */ +angular.mock.$IntervalProvider = function() { + this.$get = ['$browser', '$rootScope', '$q', '$$q', + function($browser, $rootScope, $q, $$q) { + var repeatFns = [], + nextRepeatId = 0, + now = 0; + + var $interval = function(fn, delay, count, invokeApply) { + var hasParams = arguments.length > 4, + args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], + iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; + + count = (angular.isDefined(count)) ? count : 0; + promise.then(null, null, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); + + promise.$$intervalId = nextRepeatId; + + function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + var fnIndex; + deferred.resolve(iteration); + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (angular.isDefined(fnIndex)) { + repeatFns.splice(fnIndex, 1); + } + } + + if (skipApply) { + $browser.defer.flush(); + } else { + $rootScope.$apply(); + } + } + + repeatFns.push({ + nextTime:(now + delay), + delay: delay, + fn: tick, + id: nextRepeatId, + deferred: deferred + }); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); + + nextRepeatId++; + return promise; + }; + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {promise} promise A promise from calling the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully cancelled. + */ + $interval.cancel = function(promise) { + if (!promise) return false; + var fnIndex; + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (angular.isDefined(fnIndex)) { + repeatFns[fnIndex].deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + /** + * @ngdoc method + * @name $interval#flush + * @description + * + * Runs interval tasks scheduled to be run in the next `millis` milliseconds. + * + * @param {number=} millis maximum timeout amount to flush up until. + * + * @return {number} The amount of time moved forward. + */ + $interval.flush = function(millis) { + now += millis; + while (repeatFns.length && repeatFns[0].nextTime <= now) { + var task = repeatFns[0]; + task.fn(); + task.nextTime += task.delay; + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); + } + return millis; + }; + + return $interval; + }]; +}; + + +/* jshint -W101 */ +/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! + * This directive should go inside the anonymous function but a bug in JSHint means that it would + * not be enacted early enough to prevent the warning. + */ +var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; + +function jsonStringToDate(string) { + var match; + if (match = string.match(R_ISO8061_STR)) { + var date = new Date(0), + tzHour = 0, + tzMin = 0; + if (match[9]) { + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); + } + date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + date.setUTCHours(toInt(match[4] || 0) - tzHour, + toInt(match[5] || 0) - tzMin, + toInt(match[6] || 0), + toInt(match[7] || 0)); + return date; + } + return string; +} + +function toInt(str) { + return parseInt(str, 10); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while (num.length < digits) num = '0' + num; + if (trim) { + num = num.substr(num.length - digits); + } + return neg + num; +} + + +/** + * @ngdoc type + * @name angular.mock.TzDate + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. + * + * Mock of the Date type which has its timezone specified via constructor arg. + * + * The main purpose is to create Date-like instances with timezone fixed to the specified timezone + * offset, so that we can test code that depends on local timezone settings without dependency on + * the time zone settings of the machine where the code is running. + * + * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) + * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* + * + * @example + * !!!! WARNING !!!!! + * This is not a complete Date object so only methods that were implemented can be called safely. + * To make matters worse, TzDate instances inherit stuff from Date via a prototype. + * + * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is + * incomplete we might be missing some non-standard methods. This can result in errors like: + * "Date.prototype.foo called on incompatible Object". + * + * ```js + * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); + * newYearInBratislava.getTimezoneOffset() => -60; + * newYearInBratislava.getFullYear() => 2010; + * newYearInBratislava.getMonth() => 0; + * newYearInBratislava.getDate() => 1; + * newYearInBratislava.getHours() => 0; + * newYearInBratislava.getMinutes() => 0; + * newYearInBratislava.getSeconds() => 0; + * ``` + * + */ +angular.mock.TzDate = function(offset, timestamp) { + var self = new Date(0); + if (angular.isString(timestamp)) { + var tsStr = timestamp; + + self.origDate = jsonStringToDate(timestamp); + + timestamp = self.origDate.getTime(); + if (isNaN(timestamp)) { + throw { + name: "Illegal Argument", + message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" + }; + } + } else { + self.origDate = new Date(timestamp); + } + + var localOffset = new Date(timestamp).getTimezoneOffset(); + self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60; + self.date = new Date(timestamp + self.offsetDiff); + + self.getTime = function() { + return self.date.getTime() - self.offsetDiff; + }; + + self.toLocaleDateString = function() { + return self.date.toLocaleDateString(); + }; + + self.getFullYear = function() { + return self.date.getFullYear(); + }; + + self.getMonth = function() { + return self.date.getMonth(); + }; + + self.getDate = function() { + return self.date.getDate(); + }; + + self.getHours = function() { + return self.date.getHours(); + }; + + self.getMinutes = function() { + return self.date.getMinutes(); + }; + + self.getSeconds = function() { + return self.date.getSeconds(); + }; + + self.getMilliseconds = function() { + return self.date.getMilliseconds(); + }; + + self.getTimezoneOffset = function() { + return offset * 60; + }; + + self.getUTCFullYear = function() { + return self.origDate.getUTCFullYear(); + }; + + self.getUTCMonth = function() { + return self.origDate.getUTCMonth(); + }; + + self.getUTCDate = function() { + return self.origDate.getUTCDate(); + }; + + self.getUTCHours = function() { + return self.origDate.getUTCHours(); + }; + + self.getUTCMinutes = function() { + return self.origDate.getUTCMinutes(); + }; + + self.getUTCSeconds = function() { + return self.origDate.getUTCSeconds(); + }; + + self.getUTCMilliseconds = function() { + return self.origDate.getUTCMilliseconds(); + }; + + self.getDay = function() { + return self.date.getDay(); + }; + + // provide this method only on browsers that already have it + if (self.toISOString) { + self.toISOString = function() { + return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + + padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumber(self.origDate.getUTCDate(), 2) + 'T' + + padNumber(self.origDate.getUTCHours(), 2) + ':' + + padNumber(self.origDate.getUTCMinutes(), 2) + ':' + + padNumber(self.origDate.getUTCSeconds(), 2) + '.' + + padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + }; + } + + //hide all methods not implemented in this mock that the Date prototype exposes + var unimplementedMethods = ['getUTCDay', + 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', + 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', + 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', + 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', + 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; + + angular.forEach(unimplementedMethods, function(methodName) { + self[methodName] = function() { + throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); + }; + }); + + return self; +}; + +//make "tzDateInstance instanceof Date" return true +angular.mock.TzDate.prototype = Date.prototype; +/* jshint +W101 */ + + +/** + * @ngdoc service + * @name $animate + * + * @description + * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods + * for testing animations. + */ +angular.mock.animate = angular.module('ngAnimateMock', ['ng']) + + .config(['$provide', function($provide) { + + $provide.factory('$$forceReflow', function() { + function reflowFn() { + reflowFn.totalReflows++; + } + reflowFn.totalReflows = 0; + return reflowFn; + }); + + $provide.factory('$$animateAsyncRun', function() { + var queue = []; + var queueFn = function() { + return function(fn) { + queue.push(fn); + }; + }; + queueFn.flush = function() { + if (queue.length === 0) return false; + + for (var i = 0; i < queue.length; i++) { + queue[i](); + } + queue = []; + + return true; + }; + return queueFn; + }); + + $provide.decorator('$$animateJs', ['$delegate', function($delegate) { + var runners = []; + + var animateJsConstructor = function() { + var animator = $delegate.apply($delegate, arguments); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } + return animator; + }; + + animateJsConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateJsConstructor; + }]); + + $provide.decorator('$animateCss', ['$delegate', function($delegate) { + var runners = []; + + var animateCssConstructor = function(element, options) { + var animator = $delegate(element, options); + runners.push(animator); + return animator; + }; + + animateCssConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateCssConstructor; + }]); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$animateCss', '$$animateJs', + '$$forceReflow', '$$animateAsyncRun', '$rootScope', + function($delegate, $timeout, $browser, $$rAF, $animateCss, $$animateJs, + $$forceReflow, $$animateAsyncRun, $rootScope) { + var animate = { + queue: [], + cancel: $delegate.cancel, + on: $delegate.on, + off: $delegate.off, + pin: $delegate.pin, + get reflows() { + return $$forceReflow.totalReflows; + }, + enabled: $delegate.enabled, + /** + * @ngdoc method + * @name $animate#closeAndFlush + * @description + * + * This method will close all pending animations (both {@link ngAnimate#javascript-based-animations Javascript} + * and {@link ngAnimate.$animateCss CSS}) and it will also flush any remaining animation frames and/or callbacks. + */ + closeAndFlush: function() { + // we allow the flush command to swallow the errors + // because depending on whether CSS or JS animations are + // used, there may not be a RAF flush. The primary flush + // at the end of this function must throw an exception + // because it will track if there were pending animations + this.flush(true); + $animateCss.$closeAndFlush(); + $$animateJs.$closeAndFlush(); + this.flush(); + }, + /** + * @ngdoc method + * @name $animate#flush + * @description + * + * This method is used to flush the pending callbacks and animation frames to either start + * an animation or conclude an animation. Note that this will not actually close an + * actively running animation (see {@link ngMock.$animate#closeAndFlush `closeAndFlush()`} for that). + */ + flush: function(hideErrors) { + $rootScope.$digest(); + + var doNextRun, somethingFlushed = false; + do { + doNextRun = false; + + if ($$rAF.queue.length) { + $$rAF.flush(); + doNextRun = somethingFlushed = true; + } + + if ($$animateAsyncRun.flush()) { + doNextRun = somethingFlushed = true; + } + } while (doNextRun); + + if (!somethingFlushed && !hideErrors) { + throw new Error('No pending animations ready to be closed or flushed'); + } + + $rootScope.$digest(); + } + }; + + angular.forEach( + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { + animate[method] = function() { + animate.queue.push({ + event: method, + element: arguments[0], + options: arguments[arguments.length - 1], + args: arguments + }); + return $delegate[method].apply($delegate, arguments); + }; + }); + + return animate; + }]); + + }]); + + +/** + * @ngdoc function + * @name angular.mock.dump + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available function. + * + * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for + * debugging. + * + * This method is also available on window, where it can be used to display objects on debug + * console. + * + * @param {*} object - any object to turn into string. + * @return {string} a serialized string of the argument + */ +angular.mock.dump = function(object) { + return serialize(object); + + function serialize(object) { + var out; + + if (angular.isElement(object)) { + object = angular.element(object); + out = angular.element('
'); + angular.forEach(object, function(element) { + out.append(angular.element(element).clone()); + }); + out = out.html(); + } else if (angular.isArray(object)) { + out = []; + angular.forEach(object, function(o) { + out.push(serialize(o)); + }); + out = '[ ' + out.join(', ') + ' ]'; + } else if (angular.isObject(object)) { + if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { + out = serializeScope(object); + } else if (object instanceof Error) { + out = object.stack || ('' + object.name + ': ' + object.message); + } else { + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects + out = angular.toJson(object, true); + } + } else { + out = String(object); + } + + return out; + } + + function serializeScope(scope, offset) { + offset = offset || ' '; + var log = [offset + 'Scope(' + scope.$id + '): {']; + for (var key in scope) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { + log.push(' ' + key + ': ' + angular.toJson(scope[key])); + } + } + var child = scope.$$childHead; + while (child) { + log.push(serializeScope(child, offset + ' ')); + child = child.$$nextSibling; + } + log.push('}'); + return log.join('\n' + offset); + } +}; + +/** + * @ngdoc service + * @name $httpBackend + * @description + * Fake HTTP backend implementation suitable for unit testing applications that use the + * {@link ng.$http $http service}. + * + * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. + * + * During unit testing, we want our unit tests to run quickly and have no external dependencies so + * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or + * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is + * to verify whether a certain request has been sent or not, or alternatively just let the + * application make requests, respond with pre-trained responses and assert that the end result is + * what we expect it to be. + * + * This mock implementation can be used to respond with static or dynamic responses via the + * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). + * + * When an Angular application needs some data from a server, it calls the $http service, which + * sends the request to a real server using $httpBackend service. With dependency injection, it is + * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify + * the requests and respond with some testing data without sending a request to a real server. + * + * There are two ways to specify what test data should be returned as http responses by the mock + * backend when the code under test makes http requests: + * + * - `$httpBackend.expect` - specifies a request expectation + * - `$httpBackend.when` - specifies a backend definition + * + * + * ## Request Expectations vs Backend Definitions + * + * Request expectations provide a way to make assertions about requests made by the application and + * to define responses for those requests. The test will fail if the expected requests are not made + * or they are made in the wrong order. + * + * Backend definitions allow you to define a fake backend for your application which doesn't assert + * if a particular request was made or not, it just returns a trained response if a request is made. + * The test will pass whether or not the request gets made during testing. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
+ * + * In cases where both backend definitions and request expectations are specified during unit + * testing, the request expectations are evaluated first. + * + * If a request expectation has no response specified, the algorithm will search your backend + * definitions for an appropriate response. + * + * If a request didn't match any expectation or if the expectation doesn't have the response + * defined, the backend definitions are evaluated in sequential order to see if any of them match + * the request. The response from the first matched definition is returned. + * + * + * ## Flushing HTTP requests + * + * The $httpBackend used in production always responds to requests asynchronously. If we preserved + * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, + * to follow and to maintain. But neither can the testing mock respond synchronously; that would + * change the execution of the code under test. For this reason, the mock $httpBackend has a + * `flush()` method, which allows the test to explicitly flush pending requests. This preserves + * the async api of the backend, while allowing the test to execute synchronously. + * + * + * ## Unit testing with mock $httpBackend + * The following code shows how to setup and use the mock backend when unit testing a controller. + * First we create the controller under test: + * + ```js + // The module code + angular + .module('MyApp', []) + .controller('MyController', MyController); + + // The controller code + function MyController($scope, $http) { + var authToken; + + $http.get('/auth.py').then(function(response) { + authToken = response.headers('A-Token'); + $scope.user = response.data; + }); + + $scope.saveMessage = function(message) { + var headers = { 'Authorization': authToken }; + $scope.status = 'Saving...'; + + $http.post('/add-msg.py', message, { headers: headers } ).then(function(response) { + $scope.status = ''; + }).catch(function() { + $scope.status = 'Failed...'; + }); + }; + } + ``` + * + * Now we setup the mock backend and create the test specs: + * + ```js + // testing controller + describe('MyController', function() { + var $httpBackend, $rootScope, createController, authRequestHandler; + + // Set up the module + beforeEach(module('MyApp')); + + beforeEach(inject(function($injector) { + // Set up the mock http service responses + $httpBackend = $injector.get('$httpBackend'); + // backend definition common for all tests + authRequestHandler = $httpBackend.when('GET', '/auth.py') + .respond({userId: 'userX'}, {'A-Token': 'xxx'}); + + // Get hold of a scope (i.e. the root scope) + $rootScope = $injector.get('$rootScope'); + // The $controller service is used to create instances of controllers + var $controller = $injector.get('$controller'); + + createController = function() { + return $controller('MyController', {'$scope' : $rootScope }); + }; + })); + + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + + it('should fetch authentication token', function() { + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + }); + + + it('should fail authentication', function() { + + // Notice how you can change the response even after it was set + authRequestHandler.respond(401, ''); + + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + expect($rootScope.status).toBe('Failed...'); + }); + + + it('should send msg to server', function() { + var controller = createController(); + $httpBackend.flush(); + + // now you don’t care about the authentication, but + // the controller will still send the request and + // $httpBackend will respond without you having to + // specify the expectation and response for this request + + $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); + $rootScope.saveMessage('message content'); + expect($rootScope.status).toBe('Saving...'); + $httpBackend.flush(); + expect($rootScope.status).toBe(''); + }); + + + it('should send auth header', function() { + var controller = createController(); + $httpBackend.flush(); + + $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { + // check if the header was sent, if it wasn't the expectation won't + // match the request and the test will fail + return headers['Authorization'] == 'xxx'; + }).respond(201, ''); + + $rootScope.saveMessage('whatever'); + $httpBackend.flush(); + }); + }); + ``` + * + * ## Dynamic responses + * + * You define a response to a request by chaining a call to `respond()` onto a definition or expectation. + * If you provide a **callback** as the first parameter to `respond(callback)` then you can dynamically generate + * a response based on the properties of the request. + * + * The `callback` function should be of the form `function(method, url, data, headers, params)`. + * + * ### Query parameters + * + * By default, query parameters on request URLs are parsed into the `params` object. So a request URL + * of `/list?q=searchstr&orderby=-name` would set `params` to be `{q: 'searchstr', orderby: '-name'}`. + * + * ### Regex parameter matching + * + * If an expectation or definition uses a **regex** to match the URL, you can provide an array of **keys** via a + * `params` argument. The index of each **key** in the array will match the index of a **group** in the + * **regex**. + * + * The `params` object in the **callback** will now have properties with these keys, which hold the value of the + * corresponding **group** in the **regex**. + * + * This also applies to the `when` and `expect` shortcut methods. + * + * + * ```js + * $httpBackend.expect('GET', /\/user\/(.+)/, undefined, undefined, ['id']) + * .respond(function(method, url, data, headers, params) { + * // for requested url of '/user/1234' params is {id: '1234'} + * }); + * + * $httpBackend.whenPATCH(/\/user\/(.+)\/article\/(.+)/, undefined, undefined, ['user', 'article']) + * .respond(function(method, url, data, headers, params) { + * // for url of '/user/1234/article/567' params is {user: '1234', article: '567'} + * }); + * ``` + * + * ## Matching route requests + * + * For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon + * delimited matching of the url path, ignoring the query string. This allows declarations + * similar to how application routes are configured with `$routeProvider`. Because these methods convert + * the definition url to regex, declaration order is important. Combined with query parameter parsing, + * the following is possible: + * + ```js + $httpBackend.whenRoute('GET', '/users/:id') + .respond(function(method, url, data, headers, params) { + return [200, MockUserList[Number(params.id)]]; + }); + + $httpBackend.whenRoute('GET', '/users') + .respond(function(method, url, data, headers, params) { + var userList = angular.copy(MockUserList), + defaultSort = 'lastName', + count, pages, isPrevious, isNext; + + // paged api response '/v1/users?page=2' + params.page = Number(params.page) || 1; + + // query for last names '/v1/users?q=Archer' + if (params.q) { + userList = $filter('filter')({lastName: params.q}); + } + + pages = Math.ceil(userList.length / pagingLength); + isPrevious = params.page > 1; + isNext = params.page < pages; + + return [200, { + count: userList.length, + previous: isPrevious, + next: isNext, + // sort field -> '/v1/users?sortBy=firstName' + results: $filter('orderBy')(userList, params.sortBy || defaultSort) + .splice((params.page - 1) * pagingLength, pagingLength) + }]; + }); + ``` + */ +angular.mock.$HttpBackendProvider = function() { + this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; +}; + +/** + * General factory function for $httpBackend mock. + * Returns instance for unit testing (when no arguments specified): + * - passing through is disabled + * - auto flushing is disabled + * + * Returns instance for e2e testing (when `$delegate` and `$browser` specified): + * - passing through (delegating request to real backend) is enabled + * - auto flushing is enabled + * + * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) + * @param {Object=} $browser Auto-flushing enabled if specified + * @return {Object} Instance of $httpBackend mock + */ +function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { + var definitions = [], + expectations = [], + responses = [], + responsesPush = angular.bind(responses, responses.push), + copy = angular.copy; + + function createResponse(status, data, headers, statusText) { + if (angular.isFunction(status)) return status; + + return function() { + return angular.isNumber(status) + ? [status, data, headers, statusText] + : [200, status, data, headers]; + }; + } + + // TODO(vojta): change params to: method, url, data, headers, callback + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + var xhr = new MockXhr(), + expectation = expectations[0], + wasExpected = false; + + function prettyPrint(data) { + return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + ? data + : angular.toJson(data); + } + + function wrapResponse(wrapped) { + if (!$browser && timeout) { + timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); + } + + return handleResponse; + + function handleResponse() { + var response = wrapped.response(method, url, data, headers, wrapped.params(url)); + xhr.$$respHeaders = response[2]; + callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), + copy(response[3] || '')); + } + + function handleTimeout() { + for (var i = 0, ii = responses.length; i < ii; i++) { + if (responses[i] === handleResponse) { + responses.splice(i, 1); + callback(-1, undefined, ''); + break; + } + } + } + } + + if (expectation && expectation.match(method, url)) { + if (!expectation.matchData(data)) { + throw new Error('Expected ' + expectation + ' with different data\n' + + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + } + + if (!expectation.matchHeaders(headers)) { + throw new Error('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + + prettyPrint(headers)); + } + + expectations.shift(); + + if (expectation.response) { + responses.push(wrapResponse(expectation)); + return; + } + wasExpected = true; + } + + var i = -1, definition; + while ((definition = definitions[++i])) { + if (definition.match(method, url, data, headers || {})) { + if (definition.response) { + // if $browser specified, we do auto flush all requests + ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); + } else if (definition.passThrough) { + $delegate(method, url, data, callback, headers, timeout, withCredentials); + } else throw new Error('No response defined !'); + return; + } + } + throw wasExpected ? + new Error('No response defined !') : + new Error('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); + } + + /** + * @ngdoc method + * @name $httpBackend#when + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). The respond method returns the + * `requestHandler` object for possible overrides. + */ + $httpBackend.when = function(method, url, data, headers, keys) { + var definition = new MockHttpExpectation(method, url, data, headers, keys), + chain = { + respond: function(status, data, headers, statusText) { + definition.passThrough = undefined; + definition.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + + if ($browser) { + chain.passThrough = function() { + definition.response = undefined; + definition.passThrough = true; + return chain; + }; + } + + definitions.push(definition); + return chain; + }; + + /** + * @ngdoc method + * @name $httpBackend#whenGET + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenHEAD + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenDELETE + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPOST + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenPUT + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#whenJSONP + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + createShortMethods('when'); + + /** + * @ngdoc method + * @name $httpBackend#whenRoute + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #when for more info. + */ + $httpBackend.whenRoute = function(method, url) { + var pathObj = parseRoute(url); + return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys); + }; + + function parseRoute(url) { + var ret = { + regexp: url + }, + keys = ret.keys = []; + + if (!url || !angular.isString(url)) return ret; + + url = url + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { + var optional = option === '?' ? option : null; + var star = option === '*' ? option : null; + keys.push({ name: key, optional: !!optional }); + slash = slash || ''; + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (star && '(.+?)' || '([^/]+)') + + (optional || '') + + ')' + + (optional || ''); + }) + .replace(/([\/$\*])/g, '\\$1'); + + ret.regexp = new RegExp('^' + url, 'i'); + return ret; + } + + /** + * @ngdoc method + * @name $httpBackend#expect + * @description + * Creates a new request expectation. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + * + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). The respond method returns the + * `requestHandler` object for possible overrides. + */ + $httpBackend.expect = function(method, url, data, headers, keys) { + var expectation = new MockHttpExpectation(method, url, data, headers, keys), + chain = { + respond: function(status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + + expectations.push(expectation); + return chain; + }; + + /** + * @ngdoc method + * @name $httpBackend#expectGET + * @description + * Creates a new request expectation for GET requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #expect for more info. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectHEAD + * @description + * Creates a new request expectation for HEAD requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectDELETE + * @description + * Creates a new request expectation for DELETE requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPOST + * @description + * Creates a new request expectation for POST requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPUT + * @description + * Creates a new request expectation for PUT requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectPATCH + * @description + * Creates a new request expectation for PATCH requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + + /** + * @ngdoc method + * @name $httpBackend#expectJSONP + * @description + * Creates a new request expectation for JSONP requests. For more info see `expect()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives an url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. + */ + createShortMethods('expect'); + + /** + * @ngdoc method + * @name $httpBackend#expectRoute + * @description + * Creates a new request expectation that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #expect for more info. + */ + $httpBackend.expectRoute = function(method, url) { + var pathObj = parseRoute(url); + return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys); + }; + + + /** + * @ngdoc method + * @name $httpBackend#flush + * @description + * Flushes all pending requests using the trained responses. + * + * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, + * all pending requests will be flushed. If there are no pending requests when the flush method + * is called an exception is thrown (as this typically a sign of programming error). + */ + $httpBackend.flush = function(count, digest) { + if (digest !== false) $rootScope.$digest(); + if (!responses.length) throw new Error('No pending request to flush !'); + + if (angular.isDefined(count) && count !== null) { + while (count--) { + if (!responses.length) throw new Error('No more pending request to flush !'); + responses.shift()(); + } + } else { + while (responses.length) { + responses.shift()(); + } + } + $httpBackend.verifyNoOutstandingExpectation(digest); + }; + + + /** + * @ngdoc method + * @name $httpBackend#verifyNoOutstandingExpectation + * @description + * Verifies that all of the requests defined via the `expect` api were made. If any of the + * requests were not made, verifyNoOutstandingExpectation throws an exception. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + * ```js + * afterEach($httpBackend.verifyNoOutstandingExpectation); + * ``` + */ + $httpBackend.verifyNoOutstandingExpectation = function(digest) { + if (digest !== false) $rootScope.$digest(); + if (expectations.length) { + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); + } + }; + + + /** + * @ngdoc method + * @name $httpBackend#verifyNoOutstandingRequest + * @description + * Verifies that there are no outstanding requests that need to be flushed. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + * ```js + * afterEach($httpBackend.verifyNoOutstandingRequest); + * ``` + */ + $httpBackend.verifyNoOutstandingRequest = function() { + if (responses.length) { + throw new Error('Unflushed requests: ' + responses.length); + } + }; + + + /** + * @ngdoc method + * @name $httpBackend#resetExpectations + * @description + * Resets all request expectations, but preserves all backend definitions. Typically, you would + * call resetExpectations during a multiple-phase test when you want to reuse the same instance of + * $httpBackend mock. + */ + $httpBackend.resetExpectations = function() { + expectations.length = 0; + responses.length = 0; + }; + + return $httpBackend; + + + function createShortMethods(prefix) { + angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { + $httpBackend[prefix + method] = function(url, headers, keys) { + return $httpBackend[prefix](method, url, undefined, headers, keys); + }; + }); + + angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { + $httpBackend[prefix + method] = function(url, data, headers, keys) { + return $httpBackend[prefix](method, url, data, headers, keys); + }; + }); + } +} + +function MockHttpExpectation(method, url, data, headers, keys) { + + this.data = data; + this.headers = headers; + + this.match = function(m, u, d, h) { + if (method != m) return false; + if (!this.matchUrl(u)) return false; + if (angular.isDefined(d) && !this.matchData(d)) return false; + if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + return true; + }; + + this.matchUrl = function(u) { + if (!url) return true; + if (angular.isFunction(url.test)) return url.test(u); + if (angular.isFunction(url)) return url(u); + return url == u; + }; + + this.matchHeaders = function(h) { + if (angular.isUndefined(headers)) return true; + if (angular.isFunction(headers)) return headers(h); + return angular.equals(headers, h); + }; + + this.matchData = function(d) { + if (angular.isUndefined(data)) return true; + if (data && angular.isFunction(data.test)) return data.test(d); + if (data && angular.isFunction(data)) return data(d); + if (data && !angular.isString(data)) { + return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); + } + return data == d; + }; + + this.toString = function() { + return method + ' ' + url; + }; + + this.params = function(u) { + return angular.extend(parseQuery(), pathParams()); + + function pathParams() { + var keyObj = {}; + if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj; + + var m = url.exec(u); + if (!m) return keyObj; + for (var i = 1, len = m.length; i < len; ++i) { + var key = keys[i - 1]; + var val = m[i]; + if (key && val) { + keyObj[key.name || key] = val; + } + } + + return keyObj; + } + + function parseQuery() { + var obj = {}, key_value, key, + queryStr = u.indexOf('?') > -1 + ? u.substring(u.indexOf('?') + 1) + : ""; + + angular.forEach(queryStr.split('&'), function(keyValue) { + if (keyValue) { + key_value = keyValue.replace(/\+/g,'%20').split('='); + key = tryDecodeURIComponent(key_value[0]); + if (angular.isDefined(key)) { + var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if (angular.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } + } + }); + return obj; + } + function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } + } + }; +} + +function createMockXhr() { + return new MockXhr(); +} + +function MockXhr() { + + // hack for testing $http, $httpBackend + MockXhr.$$lastInstance = this; + + this.open = function(method, url, async) { + this.$$method = method; + this.$$url = url; + this.$$async = async; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; + }; + + this.send = function(data) { + this.$$data = data; + }; + + this.setRequestHeader = function(key, value) { + this.$$reqHeaders[key] = value; + }; + + this.getResponseHeader = function(name) { + // the lookup must be case insensitive, + // that's why we try two quick lookups first and full scan last + var header = this.$$respHeaders[name]; + if (header) return header; + + name = angular.lowercase(name); + header = this.$$respHeaders[name]; + if (header) return header; + + header = undefined; + angular.forEach(this.$$respHeaders, function(headerVal, headerName) { + if (!header && angular.lowercase(headerName) == name) header = headerVal; + }); + return header; + }; + + this.getAllResponseHeaders = function() { + var lines = []; + + angular.forEach(this.$$respHeaders, function(value, key) { + lines.push(key + ': ' + value); + }); + return lines.join('\n'); + }; + + this.abort = angular.noop; +} + + +/** + * @ngdoc service + * @name $timeout + * @description + * + * This service is just a simple decorator for {@link ng.$timeout $timeout} service + * that adds a "flush" and "verifyNoPendingTasks" methods. + */ + +angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) { + + /** + * @ngdoc method + * @name $timeout#flush + * @description + * + * Flushes the queue of pending tasks. + * + * @param {number=} delay maximum timeout amount to flush up until + */ + $delegate.flush = function(delay) { + $browser.defer.flush(delay); + }; + + /** + * @ngdoc method + * @name $timeout#verifyNoPendingTasks + * @description + * + * Verifies that there are no pending tasks that need to be flushed. + */ + $delegate.verifyNoPendingTasks = function() { + if ($browser.deferredFns.length) { + throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + + formatPendingTasksAsString($browser.deferredFns)); + } + }; + + function formatPendingTasksAsString(tasks) { + var result = []; + angular.forEach(tasks, function(task) { + result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); + }); + + return result.join(', '); + } + + return $delegate; +}]; + +angular.mock.$RAFDecorator = ['$delegate', function($delegate) { + var rafFn = function(fn) { + var index = rafFn.queue.length; + rafFn.queue.push(fn); + return function() { + rafFn.queue.splice(index, 1); + }; + }; + + rafFn.queue = []; + rafFn.supported = $delegate.supported; + + rafFn.flush = function() { + if (rafFn.queue.length === 0) { + throw new Error('No rAF callbacks present'); + } + + var length = rafFn.queue.length; + for (var i = 0; i < length; i++) { + rafFn.queue[i](); + } + + rafFn.queue = rafFn.queue.slice(i); + }; + + return rafFn; +}]; + +/** + * + */ +angular.mock.$RootElementProvider = function() { + this.$get = function() { + return angular.element('
'); + }; +}; + +/** + * @ngdoc service + * @name $controller + * @description + * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing + * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. + * + * + * ## Example + * + * ```js + * + * // Directive definition ... + * + * myMod.directive('myDirective', { + * controller: 'MyDirectiveController', + * bindToController: { + * name: '@' + * } + * }); + * + * + * // Controller definition ... + * + * myMod.controller('MyDirectiveController', ['$log', function($log) { + * $log.info(this.name); + * })]; + * + * + * // In a test ... + * + * describe('myDirectiveController', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * }); + * }); + * + * ``` + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. + * + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @return {Object} Instance of given controller. + */ +angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var create = $delegate(expression, locals, true, ident); + angular.extend(create.instance, later); + return create(); + } + return $delegate(expression, locals, later, ident); + }; +}]; + +/** + * @ngdoc service + * @name $componentController + * @description + * A service that can be used to create instances of component controllers. + *
+ * Be aware that the controller will be instantiated and attached to the scope as specified in + * the component definition object. That means that you must always provide a `$scope` object + * in the `locals` param. + *
+ * @param {string} componentName the name of the component whose controller we want to instantiate + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @param {string=} ident Override the property name to use when attaching the controller to the scope. + * @return {Object} Instance of requested controller. + */ +angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) { + return { + $get: ['$controller','$injector', function($controller,$injector) { + return function $componentController(componentName, locals, bindings, ident) { + // get all directives associated to the component name + var directives = $injector.get(componentName + 'Directive'); + // look for those directives that are components + var candidateDirectives = directives.filter(function(directiveInfo) { + // components have controller, controllerAs and restrict:'E' + return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict === 'E'; + }); + // check if valid directives found + if (candidateDirectives.length === 0) { + throw new Error('No component found'); + } + if (candidateDirectives.length > 1) { + throw new Error('Too many components found'); + } + // get the info of the component + var directiveInfo = candidateDirectives[0]; + return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); + }; + }] + }; +}]; + + +/** + * @ngdoc module + * @name ngMock + * @packageName angular-mocks + * @description + * + * # ngMock + * + * The `ngMock` module provides support to inject and mock Angular services into unit tests. + * In addition, ngMock also extends various core ng services such that they can be + * inspected and controlled in a synchronous manner within test code. + * + * + *
+ * + */ +angular.module('ngMock', ['ng']).provider({ + $browser: angular.mock.$BrowserProvider, + $exceptionHandler: angular.mock.$ExceptionHandlerProvider, + $log: angular.mock.$LogProvider, + $interval: angular.mock.$IntervalProvider, + $httpBackend: angular.mock.$HttpBackendProvider, + $rootElement: angular.mock.$RootElementProvider, + $componentController: angular.mock.$ComponentControllerProvider +}).config(['$provide', function($provide) { + $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); + $provide.decorator('$$rAF', angular.mock.$RAFDecorator); + $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); + $provide.decorator('$controller', angular.mock.$ControllerDecorator); +}]); + +/** + * @ngdoc module + * @name ngMockE2E + * @module ngMockE2E + * @packageName angular-mocks + * @description + * + * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. + * Currently there is only one mock present in this module - + * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. + */ +angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { + $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); +}]); + +/** + * @ngdoc service + * @name $httpBackend + * @module ngMockE2E + * @description + * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of + * applications that use the {@link ng.$http $http service}. + * + * *Note*: For fake http backend implementation suitable for unit testing please see + * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + * + * This implementation can be used to respond with static or dynamic responses via the `when` api + * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the + * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch + * templates from a webserver). + * + * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application + * is being developed with the real backend api replaced with a mock, it is often desirable for + * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch + * templates or static files from the webserver). To configure the backend with this behavior + * use the `passThrough` request handler of `when` instead of `respond`. + * + * Additionally, we don't want to manually have to flush mocked out requests like we do during unit + * testing. For this reason the e2e $httpBackend flushes mocked out requests + * automatically, closely simulating the behavior of the XMLHttpRequest object. + * + * To setup the application to run with this http backend, you have to create a module that depends + * on the `ngMockE2E` and your application modules and defines the fake backend: + * + * ```js + * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); + * myAppDev.run(function($httpBackend) { + * phones = [{name: 'phone1'}, {name: 'phone2'}]; + * + * // returns the current list of phones + * $httpBackend.whenGET('/phones').respond(phones); + * + * // adds a new phone to the phones array + * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; + * }); + * $httpBackend.whenGET(/^\/templates\//).passThrough(); + * //... + * }); + * ``` + * + * Afterwards, bootstrap your app with this new module. + */ + +/** + * @ngdoc method + * @name $httpBackend#when + * @module ngMockE2E + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + * + * - respond – + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string), response headers + * (Object), and the text for the status (string). + * - passThrough – `{function()}` – Any request matching a backend definition with + * `passThrough` handler will be passed through to the real backend (an XHR request will be made + * to the server.) + * - Both methods return the `requestHandler` object for possible overrides. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenGET + * @module ngMockE2E + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenHEAD + * @module ngMockE2E + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenDELETE + * @module ngMockE2E + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenPOST + * @module ngMockE2E + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenPUT + * @module ngMockE2E + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenPATCH + * @module ngMockE2E + * @description + * Creates a new backend definition for PATCH requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ + +/** + * @ngdoc method + * @name $httpBackend#whenJSONP + * @module ngMockE2E + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ +/** + * @ngdoc method + * @name $httpBackend#whenRoute + * @module ngMockE2E + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ +angular.mock.e2e = {}; +angular.mock.e2e.$httpBackendDecorator = + ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; + + +/** + * @ngdoc type + * @name $rootScope.Scope + * @module ngMock + * @description + * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These + * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when + * `ngMock` module is loaded. + * + * In addition to all the regular `Scope` methods, the following helper methods are available: + */ +angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { + + var $rootScopePrototype = Object.getPrototypeOf($delegate); + + $rootScopePrototype.$countChildScopes = countChildScopes; + $rootScopePrototype.$countWatchers = countWatchers; + + return $delegate; + + // ------------------------------------------------------------------------------------------ // + + /** + * @ngdoc method + * @name $rootScope.Scope#$countChildScopes + * @module ngMock + * @description + * Counts all the direct and indirect child scopes of the current scope. + * + * The current scope is excluded from the count. The count includes all isolate child scopes. + * + * @returns {number} Total number of child scopes. + */ + function countChildScopes() { + // jshint validthis: true + var count = 0; // exclude the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += 1; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } + + + /** + * @ngdoc method + * @name $rootScope.Scope#$countWatchers + * @module ngMock + * @description + * Counts all the watchers of direct and indirect child scopes of the current scope. + * + * The watchers of the current scope are included in the count and so are all the watchers of + * isolate child scopes. + * + * @returns {number} Total number of watchers. + */ + function countWatchers() { + // jshint validthis: true + var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } +}]; + + +if (window.jasmine || window.mocha) { + + var currentSpec = null, + annotatedFunctions = [], + isSpecRunning = function() { + return !!currentSpec; + }; + + angular.mock.$$annotate = angular.injector.$$annotate; + angular.injector.$$annotate = function(fn) { + if (typeof fn === 'function' && !fn.$inject) { + annotatedFunctions.push(fn); + } + return angular.mock.$$annotate.apply(this, arguments); + }; + + + (window.beforeEach || window.setup)(function() { + annotatedFunctions = []; + currentSpec = this; + }); + + (window.afterEach || window.teardown)(function() { + var injector = currentSpec.$injector; + + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + + angular.forEach(currentSpec.$modules, function(module) { + if (module && module.$$hashKey) { + module.$$hashKey = undefined; + } + }); + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec.$providerInjector = null; + currentSpec = null; + + if (injector) { + injector.get('$rootElement').off(); + injector.get('$rootScope').$destroy(); + } + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.counter = 0; + }); + + /** + * @ngdoc function + * @name angular.mock.module + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function registers a module configuration code. It collects the configuration information + * which will be used when the injector is created by {@link angular.mock.inject inject}. + * + * See {@link angular.mock.inject inject} for usage example + * + * @param {...(string|Function|Object)} fns any number of modules which are represented as string + * aliases or as anonymous module initialization functions. The modules are used to + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed each key-value pair will be registered on the module via + * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate + * with the value on the injector. + */ + window.module = angular.mock.module = function() { + var moduleFns = Array.prototype.slice.call(arguments, 0); + return isSpecRunning() ? workFn() : workFn; + ///////////////////// + function workFn() { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not register a module!'); + } else { + var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); + angular.forEach(moduleFns, function(module) { + if (angular.isObject(module) && !angular.isArray(module)) { + fn = function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }; + } else { + fn = module; + } + if (currentSpec.$providerInjector) { + currentSpec.$providerInjector.invoke(fn); + } else { + modules.push(fn); + } + }); + } + } + }; + + /** + * @ngdoc function + * @name angular.mock.inject + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * The inject function wraps a function into an injectable function. The inject() creates new + * instance of {@link auto.$injector $injector} per test, which is then used for + * resolving references. + * + * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as _myService_, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example + * Example of what a typical jasmine tests looks like with the inject method. + * ```js + * + * angular.module('myApplicationModule', []) + * .value('mode', 'app') + * .value('version', 'v1.0.1'); + * + * + * describe('MyApp', function() { + * + * // You need to load modules that you want to test, + * // it loads only the "ng" module by default. + * beforeEach(module('myApplicationModule')); + * + * + * // inject() is used to inject arguments of all given functions + * it('should provide a version', inject(function(mode, version) { + * expect(version).toEqual('v1.0.1'); + * expect(mode).toEqual('app'); + * })); + * + * + * // The inject and module method can also be used inside of the it or beforeEach + * it('should override a version and test the new version is injected', function() { + * // module() takes functions or strings (module aliases) + * module(function($provide) { + * $provide.value('version', 'overridden'); // override version here + * }); + * + * inject(function(version) { + * expect(version).toEqual('overridden'); + * }); + * }); + * }); + * + * ``` + * + * @param {...Function} fns any number of functions which will be injected using the injector. + */ + + + + var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { + this.message = e.message; + this.name = e.name; + if (e.line) this.line = e.line; + if (e.sourceId) this.sourceId = e.sourceId; + if (e.stack && errorForStack) + this.stack = e.stack + '\n' + errorForStack.stack; + if (e.stackArray) this.stackArray = e.stackArray; + }; + ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; + + window.inject = angular.mock.inject = function() { + var blockFns = Array.prototype.slice.call(arguments, 0); + var errorForStack = new Error('Declaration Location'); + return isSpecRunning() ? workFn.call(currentSpec) : workFn; + ///////////////////// + function workFn() { + var modules = currentSpec.$modules || []; + var strictDi = !!currentSpec.$injectorStrict; + modules.unshift(function($injector) { + currentSpec.$providerInjector = $injector; + }); + modules.unshift('ngMock'); + modules.unshift('ng'); + var injector = currentSpec.$injector; + if (!injector) { + if (strictDi) { + // If strictDi is enabled, annotate the providerInjector blocks + angular.forEach(modules, function(moduleFn) { + if (typeof moduleFn === "function") { + angular.injector.$$annotate(moduleFn); + } + }); + } + injector = currentSpec.$injector = angular.injector(modules, strictDi); + currentSpec.$injectorStrict = strictDi; + } + for (var i = 0, ii = blockFns.length; i < ii; i++) { + if (currentSpec.$injectorStrict) { + // If the injector is strict / strictDi, and the spec wants to inject using automatic + // annotation, then annotate the function here. + injector.annotate(blockFns[i]); + } + try { + /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ + injector.invoke(blockFns[i] || angular.noop, this); + /* jshint +W040 */ + } catch (e) { + if (e.stack && errorForStack) { + throw new ErrorAddingDeclarationLocationStack(e, errorForStack); + } + throw e; + } finally { + errorForStack = null; + } + } + } + }; + + + angular.mock.inject.strictDi = function(value) { + value = arguments.length ? !!value : true; + return isSpecRunning() ? workFn() : workFn; + + function workFn() { + if (value !== currentSpec.$injectorStrict) { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not modify strict annotations'); + } else { + currentSpec.$injectorStrict = value; + } + } + } + }; +} + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-resource.js b/src/main/resources/static/lib/angular/angular-resource.js new file mode 100644 index 00000000..444be83c --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-resource.js @@ -0,0 +1,768 @@ +/** + * @license AngularJS v1.5.0 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $resourceMinErr = angular.$$minErr('$resource'); + +// Helper functions and regex to lookup a dotted path on an object +// stopping at undefined/null. The path must be composed of ASCII +// identifiers (just like $parse) +var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; + +function isValidDottedPath(path) { + return (path != null && path !== '' && path !== 'hasOwnProperty' && + MEMBER_NAME_REGEX.test('.' + path)); +} + +function lookupDottedPath(obj, path) { + if (!isValidDottedPath(path)) { + throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); + } + var keys = path.split('.'); + for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) { + var key = keys[i]; + obj = (obj !== null) ? obj[key] : undefined; + } + return obj; +} + +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key) { + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + + return dst; +} + +/** + * @ngdoc module + * @name ngResource + * @description + * + * # ngResource + * + * The `ngResource` module provides interaction support with RESTful services + * via the $resource service. + * + * + *
+ * + * See {@link ngResource.$resource `$resource`} for usage. + */ + +/** + * @ngdoc service + * @name $resource + * @requires $http + * @requires ng.$log + * @requires $q + * @requires ng.$timeout + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link ng.$http $http} service. + * + * Requires the {@link ngResource `ngResource`} module to be installed. + * + * By default, trailing slashes will be stripped from the calculated URLs, + * which can pose problems with server backends that do not expect that + * behavior. This can be disabled by configuring the `$resourceProvider` like + * this: + * + * ```js + app.config(['$resourceProvider', function($resourceProvider) { + // Don't strip trailing slashes from calculated URLs + $resourceProvider.defaults.stripTrailingSlashes = false; + }]); + * ``` + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + * `/user/:username`. If you are using a URL with a port number (e.g. + * `http://example.com:8080/api`), it will be respected. + * + * If you are using a url with a suffix, just add the suffix, like this: + * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` + * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be + * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you + * can escape it with `/\.`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. If a parameter value is a function, it will be executed every time + * when a param value needs to be obtained for a request (unless the param was overridden). + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@` then the value for that parameter will be extracted + * from the corresponding property on the `data` object (provided when calling an action method). + * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of + * `someParam` will be `data.someProp`. + * + * @param {Object.=} actions Hash with declaration of custom actions that should extend + * the default set of resource actions. The declaration should be created in the format of {@link + * ng.$http#usage $http.config}: + * + * {action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ...} + * + * Where: + * + * - **`action`** – {string} – The name of action. This name becomes the name of the method on + * your resource object. + * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, + * `DELETE`, `JSONP`, etc). + * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of + * the parameter value is a function, it will be executed every time when a param value needs to + * be obtained for a request (unless the param was overridden). + * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * like for the resource-level urls. + * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, + * see `returns` section. + * - **`transformRequest`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * By default, transformRequest will contain one function that checks if the request data is + * an object and serializes to using `angular.toJson`. To prevent this behavior, set + * `transformRequest` to an empty array: `transformRequest: []` + * - **`transformResponse`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * By default, transformResponse will contain one function that checks if the response looks + * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, + * set `transformResponse` to an empty array: `transformResponse: []` + * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **`timeout`** – `{number}` – timeout in milliseconds.
+ * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are + * **not** supported in $resource, because the same value would be used for multiple requests. + * If you are looking for a way to cancel requests, you should use the `cancellable` option. + * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call + * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's + * return value. Calling `$cancelRequest()` for a non-cancellable or an already + * completed/cancelled request will have no effect.
+ * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See + * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * for more information. + * - **`responseType`** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - + * `response` and `responseError`. Both `response` and `responseError` interceptors get called + * with `http response` object. See {@link ng.$http $http interceptors}. + * + * @param {Object} options Hash with custom settings that should extend the + * default `$resourceProvider` behavior. The supported options are: + * + * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing + * slashes from any calculated URL will be stripped. (Defaults to true.) + * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. + * This can be overwritten per action. (Defaults to false.) + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * ```js + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * ``` + * + * Calling these methods invoke an {@link ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class. The actions `save`, `remove` and `delete` are available on it + * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, + * read, update, delete) on server-side data like this: + * ```js + * var User = $resource('/user/:userId', {userId:'@id'}); + * var user = User.get({userId:123}, function() { + * user.abc = true; + * user.$save(); + * }); + * ``` + * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most cases one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * + * + * Success callback is called with (value, responseHeaders) arguments, where the value is + * the populated resource instance or collection object. The error callback is called + * with (httpResponse) argument. + * + * Class actions return empty instance (with additional properties below). + * Instance actions return promise of the action. + * + * The Resource instances and collections have these additional properties: + * + * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * instance or collection. + * + * On success, the promise is resolved with the same resource instance or collection object, + * updated with data from server. This makes it easy to use in + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * rendering until the resource(s) are loaded. + * + * On failure, the promise is rejected with the {@link ng.$http http response} object, without + * the `resource` property. + * + * If an interceptor object was provided, the promise will instead be resolved with the value + * returned by the interceptor. + * + * - `$resolved`: `true` after first server interaction is completed (either with success or + * rejection), `false` before that. Knowing if the Resource has been resolved is useful in + * data-binding. + * + * The Resource instances and collections have these additional methods: + * + * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or + * collection, calling this method will abort the request. + * + * @example + * + * # Credit card resource + * + * ```js + // Define CreditCard class + var CreditCard = $resource('/user/:userId/card/:cardId', + {userId:123, cardId:'@id'}, { + charge: {method:'POST', params:{charge:true}} + }); + + // We can retrieve a collection from the server + var cards = CreditCard.query(function() { + // GET: /user/123/card + // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + + var card = cards[0]; + // each item is an instance of CreditCard + expect(card instanceof CreditCard).toEqual(true); + card.name = "J. Smith"; + // non GET methods are mapped onto the instances + card.$save(); + // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} + // server returns: {id:456, number:'1234', name: 'J. Smith'}; + + // our custom method is mapped as well. + card.$charge({amount:9.99}); + // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + }); + + // we can create an instance as well + var newCard = new CreditCard({number:'0123'}); + newCard.name = "Mike Smith"; + newCard.$save(); + // POST: /user/123/card {number:'0123', name:'Mike Smith'} + // server returns: {id:789, number:'0123', name: 'Mike Smith'}; + expect(newCard.id).toEqual(789); + * ``` + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and + * `headers`. + * + * @example + * + * # User resource + * + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user) { + user.abc = true; + user.$save(); + }); + ``` + * + * It's worth noting that the success callback for `get`, `query` and other methods gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user, getResponseHeaders){ + user.abc = true; + user.$save(function(user, putResponseHeaders) { + //user => saved user object + //putResponseHeaders => $http header getter + }); + }); + ``` + * + * You can also access the raw `$http` promise via the `$promise` property on the object returned + * + ``` + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}) + .$promise.then(function(user) { + $scope.user = user; + }); + ``` + * + * @example + * + * # Creating a custom 'PUT' request + * + * In this example we create a custom method on our resource to make a PUT request + * ```js + * var app = angular.module('app', ['ngResource', 'ngRoute']); + * + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { + * return $resource('/notes/:id', null, + * { + * 'update': { method:'PUT' } + * }); + * }]); + * + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + function($scope, $routeParams, Notes) { + * // First get a note object from the factory + * var note = Notes.get({ id:$routeParams.id }); + * $id = note.id; + * + * // Now call update passing in the ID first then the object you are updating + * Notes.update({ id:$id }, note); + * + * // This will PUT /notes/ID with the note object in the request payload + * }]); + * ``` + * + * @example + * + * # Cancelling requests + * + * If an action's configuration specifies that it is cancellable, you can cancel the request related + * to an instance or collection (as long as it is a result of a "non-instance" call): + * + ```js + // ...defining the `Hotel` resource... + var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { + // Let's make the `query()` method cancellable + query: {method: 'get', isArray: true, cancellable: true} + }); + + // ...somewhere in the PlanVacationController... + ... + this.onDestinationChanged = function onDestinationChanged(destination) { + // We don't care about any pending request for hotels + // in a different destination any more + this.availableHotels.$cancelRequest(); + + // Let's query for hotels in '' + // (calls: /api/hotel?location=) + this.availableHotels = Hotel.query({location: destination}); + }; + ``` + * + */ +angular.module('ngResource', ['ng']). + provider('$resource', function() { + var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/; + var provider = this; + + this.defaults = { + // Strip slashes by default + stripTrailingSlashes: true, + + // Default actions configuration + actions: { + 'get': {method: 'GET'}, + 'save': {method: 'POST'}, + 'query': {method: 'GET', isArray: true}, + 'remove': {method: 'DELETE'}, + 'delete': {method: 'DELETE'} + } + }; + + this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { + + var noop = angular.noop, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isFunction = angular.isFunction; + + /** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set + * (pchar) allowed in path segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } + + + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a + * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't + * have to be encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } + + function Route(template, defaults) { + this.template = template; + this.defaults = extend({}, provider.defaults, defaults); + this.urlParams = {}; + } + + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, + url = actionUrl || self.template, + val, + encodedVal, + protocolAndDomain = ''; + + var urlParams = self.urlParams = {}; + forEach(url.split(/\W/), function(param) { + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } + if (!(new RegExp("^\\d+$").test(param)) && param && + (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { + urlParams[param] = { + isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url) + }; + } + }); + url = url.replace(/\\:/g, ':'); + url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { + protocolAndDomain = match; + return ''; + }); + + params = params || {}; + forEach(self.urlParams, function(paramInfo, urlParam) { + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (angular.isDefined(val) && val !== null) { + if (paramInfo.isQueryParamValue) { + encodedVal = encodeUriQuery(val, true); + } else { + encodedVal = encodeUriSegment(val); + } + url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) == '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); + + // strip trailing slashes and set the url (unless this behavior is specifically disabled) + if (self.defaults.stripTrailingSlashes) { + url = url.replace(/\/+$/, '') || '/'; + } + + // then replace collapse `/.` if found in the last URL path segment before the query + // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // replace escaped `/\.` with `/.` + config.url = protocolAndDomain + url.replace(/\/\\\./, '/.'); + + + // set params - delegate param encoding to $http + forEach(params, function(value, key) { + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; + + + function resourceFactory(url, paramDefaults, actions, options) { + var route = new Route(url, options); + + actions = extend({}, provider.defaults.actions, actions); + + function extractParams(data, actionParams) { + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key) { + if (isFunction(value)) { value = value(); } + ids[key] = value && value.charAt && value.charAt(0) == '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value) { + shallowClearAndCopy(value || {}, this); + } + + Resource.prototype.toJSON = function() { + var data = extend({}, this); + delete data.$promise; + delete data.$resolved; + return data; + }; + + forEach(actions, function(action, name) { + var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + var numericTimeout = action.timeout; + var cancellable = angular.isDefined(action.cancellable) ? action.cancellable : + (options && angular.isDefined(options.cancellable)) ? options.cancellable : + provider.defaults.cancellable; + + if (numericTimeout && !angular.isNumber(numericTimeout)) { + $log.debug('ngResource:\n' + + ' Only numeric values are allowed as `timeout`.\n' + + ' Promises are not supported in $resource, because the same value would ' + + 'be used for multiple requests. If you are looking for a way to cancel ' + + 'requests, you should use the `cancellable` option.'); + delete action.timeout; + numericTimeout = null; + } + + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; + + /* jshint -W086 */ /* (purposefully fall through case statements) */ + switch (arguments.length) { + case 4: + error = a4; + success = a3; + //fallthrough + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + "Expected up to 4 arguments [params, data, success, error], got {0} arguments", + arguments.length); + } + /* jshint +W086 */ /* (purposefully fall through case statements) */ + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + var timeoutDeferred; + var numericTimeoutPromise; + + forEach(action, function(value, key) { + switch (key) { + default: + httpConfig[key] = copy(value); + break; + case 'params': + case 'isArray': + case 'interceptor': + case 'cancellable': + break; + } + }); + + if (!isInstanceCall && cancellable) { + timeoutDeferred = $q.defer(); + httpConfig.timeout = timeoutDeferred.promise; + + if (numericTimeout) { + numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); + } + } + + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); + + var promise = $http(httpConfig).then(function(response) { + var data = response.data; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + // jshint -W018 + if (angular.isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', + 'Error in resource configuration for action `{0}`. Expected response to ' + + 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', + angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); + } + // jshint +W018 + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + if (typeof item === "object") { + value.push(new Resource(item)); + } else { + // Valid JSON values may be string literals, and these should not be converted + // into objects. These items will not have access to the Resource prototype + // methods, but unfortunately there + value.push(item); + } + }); + } else { + var promise = value.$promise; // Save the promise + shallowClearAndCopy(data, value); + value.$promise = promise; // Restore the promise + } + } + response.resource = value; + + return response; + }, function(response) { + (error || noop)(response); + return $q.reject(response); + }); + + promise.finally(function() { + value.$resolved = true; + if (!isInstanceCall && cancellable) { + value.$cancelRequest = angular.noop; + $timeout.cancel(numericTimeoutPromise); + timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; + } + }); + + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success || noop)(value, response.headers); + return value; + }, + responseErrorInterceptor); + + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; + if (cancellable) value.$cancelRequest = timeoutDeferred.resolve; + + return value; + } + + // instance call + return promise; + }; + + + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); + + Resource.bind = function(additionalParamDefaults) { + return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + return Resource; + } + + return resourceFactory; + }]; + }); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-resource.min.js b/src/main/resources/static/lib/angular/angular-resource.min.js new file mode 100644 index 00000000..306657dc --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-resource.min.js @@ -0,0 +1,15 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(Q,d,G){'use strict';function H(t,g){g=g||{};d.forEach(g,function(d,q){delete g[q]});for(var q in t)!t.hasOwnProperty(q)||"$"===q.charAt(0)&&"$"===q.charAt(1)||(g[q]=t[q]);return g}var z=d.$$minErr("$resource"),N=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;d.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,g=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}}; +this.$get=["$http","$log","$q","$timeout",function(q,M,I,J){function A(d,h){return encodeURIComponent(d).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,h?"%20":"+")}function B(d,h){this.template=d;this.defaults=v({},g.defaults,h);this.urlParams={}}function K(e,h,n,k){function c(a,b){var c={};b=v({},h,b);u(b,function(b,h){x(b)&&(b=b());var f;if(b&&b.charAt&&"@"==b.charAt(0)){f=a;var l=b.substr(1);if(null==l||""===l||"hasOwnProperty"===l||!N.test("."+ +l))throw z("badmember",l);for(var l=l.split("."),m=0,k=l.length;m + *
+ * **Note:** If your scope already contains a property with this name, it will be hidden + * or overwritten. Make sure, you specify an appropriate name for this property, that + * does not collide with other properties on the scope. + *
+ * The map object is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. * - `factory` - `{string|function}`: If `string` then it is an alias for a service. @@ -116,7 +125,10 @@ function $RouteProvider(){ * `ngRoute.$routeParams` will still refer to the previous route within these resolve * functions. Use `$route.current.params` to access the new route parameters, instead. * - * - `redirectTo` – {(string|function())=} – value to update + * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on + * the scope of the route. If omitted, defaults to `$resolve`. + * + * - `redirectTo` – `{(string|function())=}` – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -129,13 +141,13 @@ function $RouteProvider(){ * The custom `redirectTo` function is expected to return a string which will be used * to update `$location.path()` and `$location.search()`. * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` + * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. * * If the option is set to `false` and url in the browser changes, then * `$routeUpdate` event is broadcasted on the root scope. * - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive * * If the option is set to `true`, then the particular route can be matched without being * case sensitive @@ -146,27 +158,45 @@ function $RouteProvider(){ * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { + //copy original route object to preserve params inherited from proto chain + var routeCopy = angular.copy(route); + if (angular.isUndefined(routeCopy.reloadOnSearch)) { + routeCopy.reloadOnSearch = true; + } + if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { + routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; + } routes[path] = angular.extend( - {reloadOnSearch: true}, - route, - path && pathRegExp(path, route) + routeCopy, + path && pathRegExp(path, routeCopy) ); // create redirection for trailing slashes if (path) { - var redirectPath = (path[path.length-1] == '/') - ? path.substr(0, path.length-1) - : path +'/'; + var redirectPath = (path[path.length - 1] == '/') + ? path.substr(0, path.length - 1) + : path + '/'; routes[redirectPath] = angular.extend( {redirectTo: path}, - pathRegExp(redirectPath, route) + pathRegExp(redirectPath, routeCopy) ); } return this; }; + /** + * @ngdoc property + * @name $routeProvider#caseInsensitiveMatch + * @description + * + * A boolean property indicating if routes defined + * using this provider should be matched using a case insensitive + * algorithm. Defaults to `false`. + */ + this.caseInsensitiveMatch = false; + /** * @param path {string} path * @param opts {Object} options @@ -188,7 +218,7 @@ function $RouteProvider(){ path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ + .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { var optional = option === '?' ? option : null; var star = option === '*' ? option : null; keys.push({ name: key, optional: !!optional }); @@ -216,10 +246,14 @@ function $RouteProvider(){ * Sets route definition that will be used on route change when no other route definition * is matched. * - * @param {Object} params Mapping information to be assigned to `$route.current`. + * @param {Object|string} params Mapping information to be assigned to `$route.current`. + * If called with a string, the value maps to `redirectTo`. * @returns {Object} self */ this.otherwise = function(params) { + if (typeof params === 'string') { + params = {redirectTo: params}; + } this.when(null, params); return this; }; @@ -230,10 +264,9 @@ function $RouteProvider(){ '$routeParams', '$q', '$injector', - '$http', - '$templateCache', + '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { /** * @ngdoc service @@ -244,7 +277,7 @@ function $RouteProvider(){ * @property {Object} current Reference to the current route definition. * The route definition contains: * - * - `controller`: The controller constructor as define in route definition. + * - `controller`: The controller constructor as defined in the route definition. * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for * controller instantiation. The `locals` contain * the resolved values of the `resolve` map. Additionally the `locals` also contain: @@ -252,6 +285,10 @@ function $RouteProvider(){ * - `$scope` - The current route scope. * - `$template` - The current route template HTML. * + * The `locals` will be assigned to the route scope's `$resolve` property. You can override + * the property name, using `resolveAs` in the route definition. See + * {@link ngRoute.$routeProvider $routeProvider} for more info. + * * @property {Object} routes Object with all route configuration Objects as its properties. * * @description @@ -270,9 +307,6 @@ function $RouteProvider(){ * This example shows how changing the URL hash causes the `$route` to match a route against the * URL, and the `ngView` pulls in the partial. * - * Note that this example is using {@link ng.directive:script inlined templates} - * to get it working on jsfiddle as well. - * * * @@ -380,6 +414,10 @@ function $RouteProvider(){ * defined in `resolve` route property. Once all of the dependencies are resolved * `$routeChangeSuccess` is fired. * + * The route change (and the `$location` change that triggered it) can be prevented + * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} + * for more details about event object. + * * @param {Object} angularEvent Synthetic event object. * @param {Route} next Future route information. * @param {Route} current Current route information. @@ -390,7 +428,9 @@ function $RouteProvider(){ * @name $route#$routeChangeSuccess * @eventType broadcast on root scope * @description - * Broadcasted after a route dependencies are resolved. + * Broadcasted after a route change has happened successfully. + * The `resolve` dependencies are now available in the `current.locals` property. + * * {@link ngRoute.directive:ngView ngView} listens for the directive * to instantiate the controller and render the view. * @@ -418,12 +458,16 @@ function $RouteProvider(){ * @name $route#$routeUpdate * @eventType broadcast on root scope * @description - * * The `reloadOnSearch` property has been set to false, and we are reusing the same * instance of the Controller. + * + * @param {Object} angularEvent Synthetic event object + * @param {Route} current Current/previous route information. */ var forceReload = false, + preparedRoute, + preparedRouteIsUpdateOnly, $route = { routes: routes, @@ -436,15 +480,52 @@ function $RouteProvider(){ * {@link ng.$location $location} hasn't changed. * * As a result of that, {@link ngRoute.directive:ngView ngView} - * creates new scope, reinstantiates the controller. + * creates new scope and reinstantiates the controller. */ reload: function() { forceReload = true; - $rootScope.$evalAsync(updateRoute); + + var fakeLocationEvent = { + defaultPrevented: false, + preventDefault: function fakePreventDefault() { + this.defaultPrevented = true; + forceReload = false; + } + }; + + $rootScope.$evalAsync(function() { + prepareRoute(fakeLocationEvent); + if (!fakeLocationEvent.defaultPrevented) commitRoute(); + }); + }, + + /** + * @ngdoc method + * @name $route#updateParams + * + * @description + * Causes `$route` service to update the current URL, replacing + * current route parameters with those specified in `newParams`. + * Provided property names that match the route's path segment + * definitions will be interpolated into the location's path, while + * remaining properties will be treated as query params. + * + * @param {!Object} newParams mapping of URL parameter names to values + */ + updateParams: function(newParams) { + if (this.current && this.current.$$route) { + newParams = angular.extend({}, this.current.params, newParams); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); + // interpolate modifies newParams, only query params are left + $location.search(newParams); + } else { + throw $routeMinErr('norout', 'Tried updating route when with no current route'); + } } }; - $rootScope.$on('$locationChangeSuccess', updateRoute); + $rootScope.$on('$locationChangeStart', prepareRoute); + $rootScope.$on('$locationChangeSuccess', commitRoute); return $route; @@ -482,36 +563,50 @@ function $RouteProvider(){ return params; } - function updateRoute() { - var next = parseRoute(), - last = $route.current; - - if (next && last && next.$$route === last.$$route - && angular.equals(next.pathParams, last.pathParams) - && !next.reloadOnSearch && !forceReload) { - last.params = next.params; - angular.copy(last.params, $routeParams); - $rootScope.$broadcast('$routeUpdate', last); - } else if (next || last) { + function prepareRoute($locationEvent) { + var lastRoute = $route.current; + + preparedRoute = parseRoute(); + preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route + && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) + && !preparedRoute.reloadOnSearch && !forceReload; + + if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { + if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { + if ($locationEvent) { + $locationEvent.preventDefault(); + } + } + } + } + + function commitRoute() { + var lastRoute = $route.current; + var nextRoute = preparedRoute; + + if (preparedRouteIsUpdateOnly) { + lastRoute.params = nextRoute.params; + angular.copy(lastRoute.params, $routeParams); + $rootScope.$broadcast('$routeUpdate', lastRoute); + } else if (nextRoute || lastRoute) { forceReload = false; - $rootScope.$broadcast('$routeChangeStart', next, last); - $route.current = next; - if (next) { - if (next.redirectTo) { - if (angular.isString(next.redirectTo)) { - $location.path(interpolate(next.redirectTo, next.params)).search(next.params) + $route.current = nextRoute; + if (nextRoute) { + if (nextRoute.redirectTo) { + if (angular.isString(nextRoute.redirectTo)) { + $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) .replace(); } else { - $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) + $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) .replace(); } } } - $q.when(next). + $q.when(nextRoute). then(function() { - if (next) { - var locals = angular.extend({}, next.resolve), + if (nextRoute) { + var locals = angular.extend({}, nextRoute.resolve), template, templateUrl; angular.forEach(locals, function(value, key) { @@ -519,19 +614,17 @@ function $RouteProvider(){ $injector.get(value) : $injector.invoke(value, null, null, key); }); - if (angular.isDefined(template = next.template)) { + if (angular.isDefined(template = nextRoute.template)) { if (angular.isFunction(template)) { - template = template(next.params); + template = template(nextRoute.params); } - } else if (angular.isDefined(templateUrl = next.templateUrl)) { + } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(next.params); + templateUrl = templateUrl(nextRoute.params); } - templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (angular.isDefined(templateUrl)) { - next.loadedTemplateUrl = templateUrl; - template = $http.get(templateUrl, {cache: $templateCache}). - then(function(response) { return response.data; }); + nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); + template = $templateRequest(templateUrl); } } if (angular.isDefined(template)) { @@ -540,18 +633,18 @@ function $RouteProvider(){ return $q.all(locals); } }). - // after route change then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - angular.copy(next.params, $routeParams); + // after route change + if (nextRoute == $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); } - $rootScope.$broadcast('$routeChangeSuccess', next, last); + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); } }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); + if (nextRoute == $route.current) { + $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } }); } @@ -581,11 +674,11 @@ function $RouteProvider(){ */ function interpolate(string, params) { var result = []; - angular.forEach((string||'').split(':'), function(segment, i) { + angular.forEach((string || '').split(':'), function(segment, i) { if (i === 0) { result.push(segment); } else { - var segmentMatch = segment.match(/(\w+)(.*)/); + var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); var key = segmentMatch[1]; result.push(params[key]); result.push(segmentMatch[2] || ''); @@ -716,7 +809,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); .view-animate-container { position:relative; height:100px!important; - position:relative; background:white; border:1px solid black; height:40px; @@ -728,7 +820,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); } .view-animate.ng-enter, .view-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; display:block; @@ -770,7 +861,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); controllerAs: 'chapter' }); - // configure html5 to get links working on jsfiddle $locationProvider.html5Mode(true); }]) .controller('MainCtrl', ['$route', '$routeParams', '$location', @@ -817,7 +907,7 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Emitted every time the ngView content is reloaded. */ ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; -function ngViewFactory( $route, $anchorScroll, $animate) { +function ngViewFactory($route, $anchorScroll, $animate) { return { restrict: 'ECA', terminal: true, @@ -826,7 +916,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { link: function(scope, $element, attr, ctrl, $transclude) { var currentScope, currentElement, - previousElement, + previousLeaveAnimation, autoScrollExp = attr.autoscroll, onloadExp = attr.onload || ''; @@ -834,19 +924,20 @@ function ngViewFactory( $route, $anchorScroll, $animate) { update(); function cleanupLastView() { - if(previousElement) { - previousElement.remove(); - previousElement = null; + if (previousLeaveAnimation) { + $animate.cancel(previousLeaveAnimation); + previousLeaveAnimation = null; } - if(currentScope) { + + if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement, function() { - previousElement = null; + if (currentElement) { + previousLeaveAnimation = $animate.leave(currentElement); + previousLeaveAnimation.then(function() { + previousLeaveAnimation = null; }); - previousElement = currentElement; currentElement = null; } } @@ -866,7 +957,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { // function is called before linking the content, which would apply child // directives to non existing elements. var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { if (angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); @@ -914,6 +1005,7 @@ function ngViewFillContentFactory($compile, $controller, $route) { $element.data('$ngControllerController', controller); $element.children().data('$ngControllerController', controller); } + scope[current.resolveAs || '$resolve'] = locals; link(scope); } diff --git a/src/main/resources/static/lib/angular/angular-route.min.js b/src/main/resources/static/lib/angular/angular-route.min.js new file mode 100644 index 00000000..4d0d0187 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-route.min.js @@ -0,0 +1,15 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(r,d,C){'use strict';function x(s,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,y){function k(){n&&(g.cancel(n),n=null);l&&(l.$destroy(),l=null);m&&(n=g.leave(m),n.then(function(){n=null}),m=null)}function z(){var b=s.current&&s.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),f=s.current;m=y(b,function(b){g.enter(b,null,m||c).then(function(){!d.isDefined(u)||u&&!a.$eval(u)||h()});k()});l=f.scope=b;l.$emit("$viewContentLoaded"); +l.$eval(v)}else k()}var l,m,n,u=b.autoscroll,v=b.onload||"";a.$on("$routeChangeSuccess",z);z()}}}function A(d,h,g){return{restrict:"ECA",priority:-400,link:function(a,c){var b=g.current,f=b.locals;c.html(f.$template);var y=d(c.contents());if(b.controller){f.$scope=a;var k=h(b.controller,f);b.controllerAs&&(a[b.controllerAs]=k);c.data("$ngControllerController",k);c.children().data("$ngControllerController",k)}a[b.resolveAs||"$resolve"]=f;y(a)}}}r=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a, +c){return d.extend(Object.create(a),c)}function h(a,d){var b=d.caseInsensitiveMatch,f={originalPath:a,regexp:a},g=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;g.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=new RegExp("^"+a+"$",b?"i":"");return f}var g={};this.when=function(a,c){var b=d.copy(c);d.isUndefined(b.reloadOnSearch)&& +(b.reloadOnSearch=!0);d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=d.extend(b,a&&h(a,b));if(a){var f="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[f]=d.extend({redirectTo:a},h(f,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,c,b,f,h,k,r){function l(b){var e= +t.current;(x=(p=n())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!v)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function m(){var w=t.current,e=p;if(x)w.params=e.params,d.copy(w.params,b),a.$broadcast("$routeUpdate",w);else if(e||w)v=!1,(t.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?c.path(u(e.redirectTo,e.params)).search(e.params).replace():c.url(e.redirectTo(e.pathParams,c.path(),c.search())).replace()),f.when(e).then(function(){if(e){var a= +d.extend({},e.resolve),b,c;d.forEach(a,function(b,e){a[e]=d.isString(b)?h.get(b):h.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(c=e.templateUrl)&&(d.isFunction(c)&&(c=c(e.params)),d.isDefined(c)&&(e.loadedTemplateUrl=r.valueOf(c),b=k(c)));d.isDefined(b)&&(a.$template=b);return f.all(a)}}).then(function(c){e==t.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,w))},function(b){e==t.current&&a.$broadcast("$routeChangeError", +e,w,b)})}function n(){var a,b;d.forEach(g,function(f,g){var q;if(q=!b){var h=c.path();q=f.keys;var l={};if(f.regexp)if(h=f.regexp.exec(h)){for(var k=1,n=h.length;k + * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * Sanitizes an html string by stripping all potentially dangerous tokens. + * + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string. + * + * The whitelist for URL sanitization of attribute values is configured using the functions + * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider + * `$compileProvider`}. + * + * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ + + +/** + * @ngdoc provider + * @name $sanitizeProvider + * + * @description + * Creates and configures {@link $sanitize} instance. + */ +function $SanitizeProvider() { + var svgEnabled = false; + + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + if (svgEnabled) { + angular.extend(validElements, svgElements); + } + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; + + + /** + * @ngdoc method + * @name $sanitizeProvider#enableSvg + * @kind function + * + * @description + * Enables a subset of svg to be supported by the sanitizer. + * + *
+ *

By enabling this setting without taking other precautions, you might expose your + * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned + * outside of the containing element and be rendered over other elements on the page (e.g. a login + * link). Such behavior can then result in phishing incidents.

+ * + *

To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg + * tags within the sanitized content:

+ * + *
+ * + *

+   *   .rootOfTheIncludedContent svg {
+   *     overflow: hidden !important;
+   *   }
+   *   
+ *
+ * + * @param {boolean=} regexp New regexp to whitelist urls with. + * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called + * without an argument or self for chaining otherwise. + */ + this.enableSvg = function(enableSvg) { + if (angular.isDefined(enableSvg)) { + svgEnabled = enableSvg; + return this; + } else { + return svgEnabled; + } + }; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = toMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = toMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. +// They can potentially allow for arbitrary javascript to be executed. See #11290 +var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan"); + +// Blocked Elements (will be stripped) +var blockedElements = toMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); + +var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function toMap(str, lowercaseKeys) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; + } + return obj; +} + +var inertBodyElement; +(function(window) { + var doc; + if (window.document && window.document.implementation) { + doc = window.document.implementation.createHTMLDocument("inert"); + } else { + throw $sanitizeMinErr('noinert', "Can't create an inert html document"); + } + var docElement = doc.documentElement || doc.getDocumentElement(); + var bodyElements = docElement.getElementsByTagName('body'); + + // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one + if (bodyElements.length === 1) { + inertBodyElement = bodyElements[0]; + } else { + var html = doc.createElement('html'); + inertBodyElement = doc.createElement('body'); + html.appendChild(inertBodyElement); + doc.appendChild(html); + } +})(window); + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (html === null || html === undefined) { + html = ''; + } else if (typeof html !== 'string') { + html = '' + html; + } + inertBodyElement.innerHTML = html; + + //mXSS protection + var mXSSAttempts = 5; + do { + if (mXSSAttempts === 0) { + throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); + } + mXSSAttempts--; + + // strip custom-namespaced attributes on IE<=11 + if (document.documentMode <= 11) { + stripCustomNsAttrs(inertBodyElement); + } + html = inertBodyElement.innerHTML; //trigger mXSS + inertBodyElement.innerHTML = html; + } while (html !== inertBodyElement.innerHTML); + + var node = inertBodyElement.firstChild; + while (node) { + switch (node.nodeType) { + case 1: // ELEMENT_NODE + handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes)); + break; + case 3: // TEXT NODE + handler.chars(node.textContent); + break; + } + + var nextNode; + if (!(nextNode = node.firstChild)) { + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + nextNode = node.nextSibling; + if (!nextNode) { + while (nextNode == null) { + node = node.parentNode; + if (node === inertBodyElement) break; + nextNode = node.nextSibling; + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + } + } + } + node = nextNode; + } + + while (node = inertBodyElement.firstChild) { + inertBodyElement.removeChild(node); + } +} + +function attrToMap(attrs) { + var map = {}; + for (var i = 0, ii = attrs.length; i < ii; i++) { + var attr = attrs[i]; + map[attr.name] = attr.value; + } + return map; +} + + +/** + * Escapes all potentially dangerous characters, so that the + * resulting string can be safely inserted into attribute or + * element text. + * @param value + * @returns {string} escaped text + */ +function encodeEntities(value) { + return value. + replace(/&/g, '&'). + replace(SURROGATE_PAIR_REGEXP, function(value) { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). + replace(NON_ALPHANUMERIC_REGEXP, function(value) { + return '&#' + value.charCodeAt(0) + ';'; + }). + replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.join('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignoreCurrentElement = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs) { + tag = angular.lowercase(tag); + if (!ignoreCurrentElement && blockedElements[tag]) { + ignoreCurrentElement = tag; + } + if (!ignoreCurrentElement && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out('>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { + out(''); + } + if (tag == ignoreCurrentElement) { + ignoreCurrentElement = false; + } + }, + chars: function(chars) { + if (!ignoreCurrentElement) { + out(encodeEntities(chars)); + } + } + }; +} + + +/** + * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare + * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want + * to allow any of these custom attributes. This method strips them all. + * + * @param node Root element to process + */ +function stripCustomNsAttrs(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var attrNode = attrs[i]; + var attrName = attrNode.name.toLowerCase(); + if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) { + node.removeAttributeNode(attrNode); + i--; + l--; + } + } + } + + var nextNode = node.firstChild; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + + nextNode = node.nextSibling; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } +} + + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in. + * @param {object|function(url)} [attributes] Add custom attributes to the link element. + * + * Can be one of: + * + * - `object`: A map of attributes + * - `function`: Takes the url as a parameter and returns a map of attributes + * + * If the map of attributes contains a value for `target`, it overrides the value of + * the target parameter. + * + * + * @returns {string} Html-linkified and {@link $sanitize sanitized} text. + * + * @usage + + * + * @example + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'">
</div>
+
+
+
linky custom attributes +
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithSingleURL = 'http://angularjs.org/'; + }]); + + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + it('should optionally add custom attributes', function() { + expect(element(by.id('linky-custom-attributes')). + element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, + MAILTO_REGEXP = /^mailto:/i; + + var linkyMinErr = angular.$$minErr('linky'); + var isString = angular.isString; + + return function(text, target, attributes) { + if (text == null || text === '') return text; + if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); + + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + var key; + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/src/main/resources/static/lib/angular/angular-sanitize.min.js b/src/main/resources/static/lib/angular/angular-sanitize.min.js new file mode 100644 index 00000000..135d5a0e --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-sanitize.min.js @@ -0,0 +1,15 @@ +/* + AngularJS v1.5.0 + (c) 2010-2016 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(A,e,B){'use strict';function C(a){var c=[];v(c,e.noop).chars(a);return c.join("")}function h(a,c){var b={},d=a.split(","),l;for(l=0;l=document.documentMode&&n(g);a=g.innerHTML;g.innerHTML=a}while(a!==g.innerHTML);for(b=g.firstChild;b;){switch(b.nodeType){case 1:c.start(b.nodeName.toLowerCase(),E(b.attributes)); +break;case 3:c.chars(b.textContent)}var d;if(!(d=b.firstChild)&&(1==b.nodeType&&c.end(b.nodeName.toLowerCase()),d=b.nextSibling,!d))for(;null==d;){b=b.parentNode;if(b===g)break;d=b.nextSibling;1==b.nodeType&&c.end(b.nodeName.toLowerCase())}b=d}for(;b=g.firstChild;)g.removeChild(b)}function E(a){for(var c={},b=0,d=a.length;b/g,">")}function v(a,c){var b=!1,d=e.bind(a,a.push);return{start:function(a,f){a=e.lowercase(a);!b&&H[a]&&(b=a);b||!0!==t[a]||(d("<"),d(a),e.forEach(f,function(b,f){var g=e.lowercase(f),h="img"===a&&"src"===g||"background"===g;!0!==I[g]||!0===y[g]&&!c(b,h)||(d(" "),d(f),d('="'),d(x(b)),d('"'))}),d(">"))},end:function(a){a=e.lowercase(a);b||!0!==t[a]||!0===z[a]||(d(""));a== +b&&(b=!1)},chars:function(a){b||d(x(a))}}}function n(a){if(a.nodeType===Node.ELEMENT_NODE)for(var c=a.attributes,b=0,d=c.length;b"\u201d\u2019]/i,b=/^mailto:/i,d=e.$$minErr("linky"),g=e.isString;return function(f,h,m){function k(a){a&&p.push(C(a))}function q(a,b){var c;p.push("');k(b);p.push("")}if(null==f||""===f)return f;if(!g(f))throw d("notstring",f);for(var r=f,p=[],s,n;f=r.match(c);)s=f[0],f[2]||f[4]||(s=(f[3]?"http://":"mailto:")+s),n=f.index,k(r.substr(0,n)),q(s,f[0].replace(b,"")),r=r.substring(n+f[0].length);k(r);return a(p.join(""))}}])})(window,window.angular); +//# sourceMappingURL=angular-sanitize.min.js.map diff --git a/src/main/resources/static/lib/angular/angular-sanitize.min.js.map b/src/main/resources/static/lib/angular/angular-sanitize.min.js.map new file mode 100644 index 00000000..7276abd2 --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-sanitize.min.js.map @@ -0,0 +1,8 @@ +{ +"version":3, +"file":"angular-sanitize.min.js", +"lineCount":14, +"mappings":"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", +"sources":["angular-sanitize.js"], +"names":["window","angular","undefined","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","toMap","str","lowercaseKeys","obj","items","split","i","length","lowercase","htmlParser","html","handler","inertBodyElement","innerHTML","mXSSAttempts","$sanitizeMinErr","document","documentMode","stripCustomNsAttrs","node","firstChild","nodeType","start","nodeName","toLowerCase","attrToMap","attributes","textContent","nextNode","end","nextSibling","parentNode","removeChild","attrs","map","ii","attr","name","value","encodeEntities","replace","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","uriValidator","ignoreCurrentElement","out","bind","push","tag","blockedElements","validElements","forEach","key","lkey","isImage","validAttrs","uriAttrs","voidElements","Node","ELEMENT_NODE","l","attrNode","attrName","indexOf","removeAttributeNode","$$minErr","optionalEndTagBlockElements","optionalEndTagInlineElements","optionalEndTagElements","extend","blockElements","inlineElements","svgElements","htmlAttrs","svgAttrs","implementation","doc","createHTMLDocument","bodyElements","getElementsByTagName","documentElement","getDocumentElement","createElement","appendChild","module","provider","$SanitizeProvider","svgEnabled","$get","$$sanitizeUri","uri","test","enableSvg","this.enableSvg","isDefined","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","linkyMinErr","isString","text","target","addText","addLink","url","isFunction","isObject","raw","match","index","substr","substring"] +} diff --git a/src/main/resources/static/lib/angular/angular-scenario.js b/src/main/resources/static/lib/angular/angular-scenario.js new file mode 100644 index 00000000..20af805f --- /dev/null +++ b/src/main/resources/static/lib/angular/angular-scenario.js @@ -0,0 +1,41849 @@ +/*! + * jQuery JavaScript Library v2.1.1 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-05-01T17:11Z + */ + +(function( global, factory ) {'use strict'; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper window is present, + // execute the factory and get jQuery + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a jQuery-making factory as module.exports + // This accentuates the need for the creation of a real window + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +// + +var arr = []; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + version = "2.1.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; + }, + + isPlainObject: function( obj ) { + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { + return false; + } + + // If the function hasn't returned already, we're confident that + // |obj| is a plain object, created by {} or constructed with new Object + return true; + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + // Support: Android < 4.0, iOS < 6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf("use strict") === 1 ) { + script = document.createElement("script"); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v1.10.19 + * http://sizzlejs.com/ + * + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-04-18 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + characterEncoding + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( documentIsHTML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document (jQuery #6963) + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // QSA path + if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = attrs.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== strundefined && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, + doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsHTML = !isXML( doc ); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if ( parent && parent !== parent.top ) { + // IE11 does not have attachEvent, so all must suffer + if ( parent.addEventListener ) { + parent.addEventListener( "unload", function() { + setDocument(); + }, false ); + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", function() { + setDocument(); + }); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { + div.innerHTML = "
"; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowclip^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return doc; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context !== document && context; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; + }); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; + }, + + sibling: function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter(function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.unique( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +}); +var rnotwhite = (/\S+/g); + + + +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +}); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[0], key ) : emptyGet; +}; + + +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( owner ) { + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + +function Data() { + // Support: Android < 4, + // Old WebKit does not have Object.preventExtensions/freeze method, + // return new empty object instead with no [[set]] accessor + Object.defineProperty( this.cache = {}, 0, { + get: function() { + return {}; + } + }); + + this.expando = jQuery.expando + Math.random(); +} + +Data.uid = 1; +Data.accepts = jQuery.acceptData; + +Data.prototype = { + key: function( owner ) { + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return the key for a frozen object. + if ( !Data.accepts( owner ) ) { + return 0; + } + + var descriptor = {}, + // Check if the owner object already has a cache key + unlock = owner[ this.expando ]; + + // If not, create one + if ( !unlock ) { + unlock = Data.uid++; + + // Secure it in a non-enumerable, non-writable property + try { + descriptor[ this.expando ] = { value: unlock }; + Object.defineProperties( owner, descriptor ); + + // Support: Android < 4 + // Fallback to a less secure definition + } catch ( e ) { + descriptor[ this.expando ] = unlock; + jQuery.extend( owner, descriptor ); + } + } + + // Ensure the cache object + if ( !this.cache[ unlock ] ) { + this.cache[ unlock ] = {}; + } + + return unlock; + }, + set: function( owner, data, value ) { + var prop, + // There may be an unlock assigned to this node, + // if there is no entry for this "owner", create one inline + // and set the unlock as though an owner entry had always existed + unlock = this.key( owner ), + cache = this.cache[ unlock ]; + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + // Fresh assignments by object are shallow copied + if ( jQuery.isEmptyObject( cache ) ) { + jQuery.extend( this.cache[ unlock ], data ); + // Otherwise, copy the properties one-by-one to the cache object + } else { + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + } + return cache; + }, + get: function( owner, key ) { + // Either a valid cache is found, or will be created. + // New caches will be created and the unlock returned, + // allowing direct access to the newly created + // empty data object. A valid owner object must be provided. + var cache = this.cache[ this.key( owner ) ]; + + return key === undefined ? + cache : cache[ key ]; + }, + access: function( owner, key, value ) { + var stored; + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ((key && typeof key === "string") && value === undefined) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase(key) ); + } + + // [*]When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + unlock = this.key( owner ), + cache = this.cache[ unlock ]; + + if ( key === undefined ) { + this.cache[ unlock ] = {}; + + } else { + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + }, + hasData: function( owner ) { + return !jQuery.isEmptyObject( + this.cache[ owner[ this.expando ] ] || {} + ); + }, + discard: function( owner ) { + if ( owner[ this.expando ] ) { + delete this.cache[ owner[ this.expando ] ]; + } + } +}; +var data_priv = new Data(); + +var data_user = new Data(); + + + +/* + Implementation Summary + + 1. Enforce API surface and semantic compatibility with 1.9.x branch + 2. Improve the module's maintainability by reducing the storage + paths to a single mechanism. + 3. Use the same single mechanism to support "private" and "user" data. + 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) + 5. Avoid exposing implementation details on user objects (eg. expando properties) + 6. Provide a clear path for implementation upgrade to WeakMap in 2014 +*/ +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + data_user.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend({ + hasData: function( elem ) { + return data_user.hasData( elem ) || data_priv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return data_user.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + data_user.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to data_priv methods, these can be deprecated. + _data: function( elem, name, data ) { + return data_priv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + data_priv.remove( elem, name ); + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = data_user.get( elem ); + + if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + data_priv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + data_user.set( this, key ); + }); + } + + return access( this, function( value ) { + var data, + camelKey = jQuery.camelCase( key ); + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + // Attempt to get data from the cache + // with the key as-is + data = data_user.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to get data from the cache + // with the key camelized + data = data_user.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each(function() { + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = data_user.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + data_user.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf("-") !== -1 && data !== undefined ) { + data_user.set( this, key, value ); + } + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + data_user.remove( this, key ); + }); + } +}); + + +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = data_priv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = data_priv.access( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return data_priv.get( elem, key ) || data_priv.access( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + data_priv.remove( elem, [ type + "queue", key ] ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = data_priv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + }; + +var rcheckableType = (/^(?:checkbox|radio)$/i); + + + +(function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // #11217 - WebKit loses check when the name is after the checked attribute + // Support: Windows Web Apps (WWA) + // `name` and `type` need .setAttribute for WWA + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE9-IE11+ + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +})(); +var strundefined = typeof undefined; + + + +support.focusinBubbles = "onfocusin" in window; + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.hasData( elem ) && data_priv.get( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + data_priv.remove( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome < 28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } +}; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: Android < 4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && e.preventDefault ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && e.stopPropagation ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// Support: Chrome 15+ +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// Create "bubbling" focus and blur events +// Support: Firefox, Chrome, Safari +if ( !support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + data_priv.remove( doc, fix ); + + } else { + data_priv.access( doc, fix, attaches ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + + // Support: IE 9 + option: [ 1, "" ], + + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] + }; + +// Support: IE 9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: 1.x compatibility +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute("type"); + } + + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + data_priv.set( + elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) + ); + } +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( data_priv.hasData( src ) ) { + pdataOld = data_priv.access( src ); + pdataCur = data_priv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( data_user.hasData( src ) ) { + udataOld = data_user.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + data_user.set( dest, udataCur ); + } +} + +function getAll( context, tag ) { + var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : + context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + +// Support: IE >= 9 +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Support: IE >= 9 + // Fix Cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Fixes #12346 + // Support: Webkit, IE + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; + }, + + cleanData: function( elems ) { + var data, elem, type, key, + special = jQuery.event.special, + i = 0; + + for ( ; (elem = elems[ i ]) !== undefined; i++ ) { + if ( jQuery.acceptData( elem ) ) { + key = elem[ data_priv.expando ]; + + if ( key && (data = data_priv.cache[ key ]) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + if ( data_priv.cache[ key ] ) { + // Discard any remaining `private` data + delete data_priv.cache[ key ]; + } + } + } + // Discard any remaining `user` data + delete data_user.cache[ elem[ data_user.expando ] ]; + } + } +}); + +jQuery.fn.extend({ + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each(function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + }); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + remove: function( selector, keepData /* Internal Use Only */ ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map(function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var arg = arguments[ 0 ]; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + arg = this.parentNode; + + jQuery.cleanData( getAll( this ) ); + + if ( arg ) { + arg.replaceChild( elem, this ); + } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + self.domManip( args, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return this; + } +}); + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + + +var iframe, + elemdisplay = {}; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? + + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery( " +
--> +
+
+ + + + +
+ + Примеры: птиц*, + НУУ-ВВУ, + клекс, + 10.10176, + 3.0844-3.0846 + + + + +

Последние добавленные видео ответы

+
+ +
+ посмотреть все → + +

Прикреплённые иллюстрации и схемы

+
+
+
+ +
{{img.name | cut:false:65}}
+
+
+
+ посмотреть все → + +

Последние добавленные аудио ответы

+ + посмотреть все → + +

Последние добавленные документы

+ {{doc.name|cut:true:75}} + посмотреть все → + + + + Оригинальные тексты автора ииссиидиологии представлены в виде нумерованных абзацев, остальной материал является результатом творчества коллектива читателей + Представиться +
{{user.name}}
+
+ \ No newline at end of file diff --git a/src/main/resources/static/partials/modal-confirm.html b/src/main/resources/static/partials/modal-confirm.html new file mode 100644 index 00000000..63343457 --- /dev/null +++ b/src/main/resources/static/partials/modal-confirm.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/partials/paragraph.html b/src/main/resources/static/partials/paragraph.html new file mode 100644 index 00000000..2675e4c0 --- /dev/null +++ b/src/main/resources/static/partials/paragraph.html @@ -0,0 +1,12 @@ +

{{from ? from + '-' + to : number}}

+ + +
Загрузка...
+

+

+ {{child.name}}. + + +

+Следующие абзацы → + diff --git a/src/main/resources/static/partials/record.html b/src/main/resources/static/partials/record.html new file mode 100644 index 00000000..9e9ee197 --- /dev/null +++ b/src/main/resources/static/partials/record.html @@ -0,0 +1,51 @@ +
+
Загрузка...
+
+
+ ← Показать все ответы +
+ +  

{{record.name}}

+ ({{record.duration | duration:'mm:ss' }}) +   +   +
+ {{record.code}} + +
+
+
+

Ответы

фильтровать +
+
+ Год: + + + + Обновить +
+ + {{recordLoading ? 'Загрузка...' : 'Загрузить ещё'}} +
+
+
+ + diff --git a/src/main/resources/static/partials/resources.html b/src/main/resources/static/partials/resources.html new file mode 100644 index 00000000..fd526663 --- /dev/null +++ b/src/main/resources/static/partials/resources.html @@ -0,0 +1,35 @@ +
+
+ +
+
+ + + + +
+
Добавление видео...
+
+
+ ← К списку видео +
+

{{video.title}}

+ + указать код + {{video.code}} + + +
+ +
+ +
+
\ No newline at end of file diff --git a/src/main/webapp/new/partials/state1.list.html b/src/main/resources/static/partials/state1.list.html similarity index 100% rename from src/main/webapp/new/partials/state1.list.html rename to src/main/resources/static/partials/state1.list.html diff --git a/src/main/webapp/new/partials/state2.html b/src/main/resources/static/partials/state2.html similarity index 100% rename from src/main/webapp/new/partials/state2.html rename to src/main/resources/static/partials/state2.html diff --git a/src/main/resources/static/partials/tagger.html b/src/main/resources/static/partials/tagger.html new file mode 100644 index 00000000..2fae9d0a --- /dev/null +++ b/src/main/resources/static/partials/tagger.html @@ -0,0 +1,19 @@ +
+ + Подсчитать термины + + Поиск терминов... + +

Выбранные термины

+
+ {{item.key}}, +
+ +

Найденные термины

+
+ +
+
\ No newline at end of file diff --git a/src/main/resources/static/partials/term-prompt.html b/src/main/resources/static/partials/term-prompt.html new file mode 100644 index 00000000..276c2247 --- /dev/null +++ b/src/main/resources/static/partials/term-prompt.html @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/partials/term.html b/src/main/resources/static/partials/term.html new file mode 100644 index 00000000..0c426510 --- /dev/null +++ b/src/main/resources/static/partials/term.html @@ -0,0 +1,292 @@ +
+ Термин + {{name}} +
+
+ + + + +
+ + +
Попробуйте со звёздочкой после корня слова для учёта всех вариантов + окончания слова. Например "лийллусцивный" - "лийллусцивн*" (будут учтены "лийллусцивно", "лийллусцивные", + "лийллусцивность" и т. д. + Если фраза будет начинаться с "!", то система будет искать фразу "как есть", то есть без дополнительных оптимизаций поиска. +
+
+
Загрузка...
+
+ Перенапраленно с {{from.name}} + Код понятия {{code.name}} + Синонимы и сокращения + + {{alias.name}}{{$last ? '' : ', '}} + + +
+ Словарная статья свободной интерпретации: +
+

+

+
+
+ + +
+ +

Избранные цитаты

показать {{quotes.length}} +
+ +
+
+ {{quote._label}} + + + + Загрузка... + +
+ +
+ +

Упоминания в содержании

показать {{categories.length}} +
+
+ {{cat.path}} +
+
+ +
+ +

Видео записи

показать {{topicResources.video.length}} +
+
+ +
+ +
+ +

Аудио ответы Ориса

показать {{topicResources.record.length}} +
+
+ +
+ +
+ +

Статьи

показать {{topicResources.document.length}} +
+
+ +
+ +
+ +

Иллюстрации и схемы

показать {{topicResources.image.length}} +
+
+
+
+ +
{{resHolder.resource.name | cut:false:65}}
+
+
+
+ + Также связан с + {{link._label}} +
+ +
+ + + diff --git a/src/main/resources/static/partials/topic-prompt.html b/src/main/resources/static/partials/topic-prompt.html new file mode 100644 index 00000000..6fbfdd39 --- /dev/null +++ b/src/main/resources/static/partials/topic-prompt.html @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/partials/topic-selector.html b/src/main/resources/static/partials/topic-selector.html new file mode 100644 index 00000000..4cc893db --- /dev/null +++ b/src/main/resources/static/partials/topic-selector.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/partials/topic-tree.html b/src/main/resources/static/partials/topic-tree.html new file mode 100644 index 00000000..365de445 --- /dev/null +++ b/src/main/resources/static/partials/topic-tree.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/main/resources/static/partials/topic.html b/src/main/resources/static/partials/topic.html new file mode 100644 index 00000000..943b5329 --- /dev/null +++ b/src/main/resources/static/partials/topic.html @@ -0,0 +1,105 @@ + +
Загрузка...
+
+ + Перейти на описание термина + + + Просмотреть похожий термин "{{suggested_term}}" + +
+
+ Дочерние ключевые слова + + +

{{child | cut:true:70}}

+ +
+
+
+
+
+ Прикреплённые видео + +
+ +
+
+ +
+
+
+ Прикреплённые абзацы + +
+
+
+ Прикреплённые иллюстрации и схемы +
+
+
+ +
{{resHolder.resource.name | cut:false:65}}
+
+
+
+
+
+ Прикреплённые документы + +
+ +
+
+ +
+ Прикреплённые аудио записи + +
+ + Искать в тексте Ииссиидиологии + +
+ diff --git a/src/main/resources/static/partials/topics-directive.html b/src/main/resources/static/partials/topics-directive.html new file mode 100644 index 00000000..f5eb630c --- /dev/null +++ b/src/main/resources/static/partials/topics-directive.html @@ -0,0 +1,46 @@ +Ключевые слова не указаны +

Ключевые слова:

+
Загрузка ключевых слов...
+ + +
+
+ + + добавить ключевое слово +
+
+ + + + +
+ + Степень значимости ключевого слова + +
+ Добавить ключевое слово + скрыть +
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/static/template/accordion/accordion-group.html b/src/main/resources/static/template/accordion/accordion-group.html new file mode 100644 index 00000000..12d1c044 --- /dev/null +++ b/src/main/resources/static/template/accordion/accordion-group.html @@ -0,0 +1,10 @@ +
+
+

+ {{heading}} +

+
+
+
+
+
\ No newline at end of file diff --git a/src/main/resources/static/template/accordion/accordion.html b/src/main/resources/static/template/accordion/accordion.html new file mode 100644 index 00000000..ba428f3b --- /dev/null +++ b/src/main/resources/static/template/accordion/accordion.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/main/resources/static/template/alert/alert.html b/src/main/resources/static/template/alert/alert.html new file mode 100644 index 00000000..64159609 --- /dev/null +++ b/src/main/resources/static/template/alert/alert.html @@ -0,0 +1,7 @@ + diff --git a/src/main/resources/static/template/carousel/carousel.html b/src/main/resources/static/template/carousel/carousel.html new file mode 100644 index 00000000..9769b383 --- /dev/null +++ b/src/main/resources/static/template/carousel/carousel.html @@ -0,0 +1,8 @@ + diff --git a/src/main/resources/static/template/carousel/slide.html b/src/main/resources/static/template/carousel/slide.html new file mode 100644 index 00000000..451e4fba --- /dev/null +++ b/src/main/resources/static/template/carousel/slide.html @@ -0,0 +1,7 @@ +
diff --git a/src/main/resources/static/template/datepicker/datepicker.html b/src/main/resources/static/template/datepicker/datepicker.html new file mode 100644 index 00000000..1ecb3c50 --- /dev/null +++ b/src/main/resources/static/template/datepicker/datepicker.html @@ -0,0 +1,5 @@ +
+ + + +
\ No newline at end of file diff --git a/src/main/resources/static/template/datepicker/day.html b/src/main/resources/static/template/datepicker/day.html new file mode 100644 index 00000000..ca212a39 --- /dev/null +++ b/src/main/resources/static/template/datepicker/day.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + +
{{label.abbr}}
{{ weekNumbers[$index] }} + +
diff --git a/src/main/resources/static/template/datepicker/month.html b/src/main/resources/static/template/datepicker/month.html new file mode 100644 index 00000000..53921900 --- /dev/null +++ b/src/main/resources/static/template/datepicker/month.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + +
+ +
diff --git a/src/main/resources/static/template/datepicker/popup.html b/src/main/resources/static/template/datepicker/popup.html new file mode 100644 index 00000000..483bbe1e --- /dev/null +++ b/src/main/resources/static/template/datepicker/popup.html @@ -0,0 +1,10 @@ + diff --git a/src/main/resources/static/template/datepicker/year.html b/src/main/resources/static/template/datepicker/year.html new file mode 100644 index 00000000..978d80c2 --- /dev/null +++ b/src/main/resources/static/template/datepicker/year.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + +
+ +
diff --git a/src/main/resources/static/template/modal/backdrop.html b/src/main/resources/static/template/modal/backdrop.html new file mode 100644 index 00000000..9cbfcb6c --- /dev/null +++ b/src/main/resources/static/template/modal/backdrop.html @@ -0,0 +1,4 @@ + diff --git a/src/main/resources/static/template/modal/window.html b/src/main/resources/static/template/modal/window.html new file mode 100644 index 00000000..81ca197e --- /dev/null +++ b/src/main/resources/static/template/modal/window.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/main/resources/static/template/pagination/pager.html b/src/main/resources/static/template/pagination/pager.html new file mode 100644 index 00000000..ca150de1 --- /dev/null +++ b/src/main/resources/static/template/pagination/pager.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/main/resources/static/template/pagination/pagination.html b/src/main/resources/static/template/pagination/pagination.html new file mode 100644 index 00000000..cd45d290 --- /dev/null +++ b/src/main/resources/static/template/pagination/pagination.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/main/resources/static/template/popover/popover.html b/src/main/resources/static/template/popover/popover.html new file mode 100644 index 00000000..5929ee6e --- /dev/null +++ b/src/main/resources/static/template/popover/popover.html @@ -0,0 +1,8 @@ +
+
+ +
+

+
+
+
diff --git a/src/main/resources/static/template/progressbar/bar.html b/src/main/resources/static/template/progressbar/bar.html new file mode 100644 index 00000000..bde46dca --- /dev/null +++ b/src/main/resources/static/template/progressbar/bar.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/main/resources/static/template/progressbar/progress.html b/src/main/resources/static/template/progressbar/progress.html new file mode 100644 index 00000000..19685370 --- /dev/null +++ b/src/main/resources/static/template/progressbar/progress.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/main/resources/static/template/progressbar/progressbar.html b/src/main/resources/static/template/progressbar/progressbar.html new file mode 100644 index 00000000..efb65033 --- /dev/null +++ b/src/main/resources/static/template/progressbar/progressbar.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/src/main/resources/static/template/rating/rating.html b/src/main/resources/static/template/rating/rating.html new file mode 100644 index 00000000..f4ab6bc7 --- /dev/null +++ b/src/main/resources/static/template/rating/rating.html @@ -0,0 +1,5 @@ + + + ({{ $index < value ? '*' : ' ' }}) + + \ No newline at end of file diff --git a/src/main/resources/static/template/tabs/tab.html b/src/main/resources/static/template/tabs/tab.html new file mode 100644 index 00000000..aedd7ef3 --- /dev/null +++ b/src/main/resources/static/template/tabs/tab.html @@ -0,0 +1,3 @@ +
  • + {{heading}} +
  • diff --git a/src/main/resources/static/template/tabs/tabset.html b/src/main/resources/static/template/tabs/tabset.html new file mode 100644 index 00000000..b953a491 --- /dev/null +++ b/src/main/resources/static/template/tabs/tabset.html @@ -0,0 +1,10 @@ +
    + +
    +
    +
    +
    +
    diff --git a/src/main/resources/static/template/timepicker/timepicker.html b/src/main/resources/static/template/timepicker/timepicker.html new file mode 100644 index 00000000..bef8e27f --- /dev/null +++ b/src/main/resources/static/template/timepicker/timepicker.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + +
     
    + + : + +
     
    diff --git a/src/main/resources/static/template/tooltip/tooltip-html-unsafe-popup.html b/src/main/resources/static/template/tooltip/tooltip-html-unsafe-popup.html new file mode 100644 index 00000000..129016d9 --- /dev/null +++ b/src/main/resources/static/template/tooltip/tooltip-html-unsafe-popup.html @@ -0,0 +1,4 @@ +
    +
    +
    +
    diff --git a/src/main/resources/static/template/tooltip/tooltip-popup.html b/src/main/resources/static/template/tooltip/tooltip-popup.html new file mode 100644 index 00000000..fd511207 --- /dev/null +++ b/src/main/resources/static/template/tooltip/tooltip-popup.html @@ -0,0 +1,4 @@ +
    +
    +
    +
    diff --git a/src/main/resources/static/template/typeahead/typeahead-match.html b/src/main/resources/static/template/typeahead/typeahead-match.html new file mode 100644 index 00000000..d79e10a1 --- /dev/null +++ b/src/main/resources/static/template/typeahead/typeahead-match.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/template/typeahead/typeahead-popup.html b/src/main/resources/static/template/typeahead/typeahead-popup.html new file mode 100644 index 00000000..e1bd0c1c --- /dev/null +++ b/src/main/resources/static/template/typeahead/typeahead-popup.html @@ -0,0 +1,5 @@ + diff --git a/src/main/resources/translation/endings.txt b/src/main/resources/translation/endings.txt new file mode 100644 index 00000000..40d83c1a --- /dev/null +++ b/src/main/resources/translation/endings.txt @@ -0,0 +1,13 @@ +а +и +я +е +й +ы +ые +ие +ую +стными +лые +ми +ую \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/applicationContext.xml b/src/main/webapp/WEB-INF/applicationContext.xml deleted file mode 100644 index 5dde254f..00000000 --- a/src/main/webapp/WEB-INF/applicationContext.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/main/webapp/adm.html b/src/main/webapp/adm.html deleted file mode 100644 index e8e5a976..00000000 --- a/src/main/webapp/adm.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - -
    -

    Связать термин с пунктом

    - - -
    - -
    - - -
    -
    -
    -

    Связать термин с термином

    - -
    - -
    - -
    - -
    - - -
    -
    -
    -

    Добавить термин

    - -
    - -
    - -
    - -
    -
    - - \ No newline at end of file diff --git a/src/main/webapp/google9ff4abadde5fb24d.html b/src/main/webapp/google9ff4abadde5fb24d.html deleted file mode 100644 index 63113613..00000000 --- a/src/main/webapp/google9ff4abadde5fb24d.html +++ /dev/null @@ -1 +0,0 @@ -google-site-verification: google9ff4abadde5fb24d.html \ No newline at end of file diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html deleted file mode 100644 index 8dc141a2..00000000 --- a/src/main/webapp/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - Интерактивная Ииссиидиология - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - -
    -
    -
    -
    - - - - - - - - - - - - diff --git a/src/main/webapp/new/css/style.css b/src/main/webapp/new/css/style.css deleted file mode 100644 index 3e76e7f1..00000000 --- a/src/main/webapp/new/css/style.css +++ /dev/null @@ -1,78 +0,0 @@ -body { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Trebuchet MS', sans-serif; - color: #00006d; -} - -div[ui-view] { - max-width: 650px; - margin: 0 auto; -} - -blockquote { - font-size: inherit; - padding: 0 10px; -} -/* home */ -.home { - margin: 0 auto; - max-width: 500px; -} -.home h1 { - text-align: center; - font-size: 180%; - color: rgb(7, 131, 199); -} -.home .header { - margin: 0 auto 30px auto; - width: 150px; - height: 145px; -} -.footer { - margin: 30px auto; - text-align: center; - /*width: 420px;*/ - /*margin-left: 100px;*/ -} -/* end home */ - -.full-width { - width: 100% -} - -/*Live Search styles start*/ -ul.searchresultspopup { - border: #a4bed4 1px solid; - margin-top: 0px; - padding: 0px; - z-index: 99999!important; - position: fixed; - background-color: white; - /*max-height:200px;*/ - border-collapse: separate; - overflow-y: hidden; -} - -ul.searchresultspopup > li { - cursor: pointer; - padding-left: 1px; - text-align: left; - list-style: none; - line-height: 20px; - list-style-image: none; - list-style-position: outside; - list-style-type: none; - text-transform: lowercase; -} - -ul.searchresultspopup li.selected, ul.searchresultspopup li:hover { - background-color: #ededed; -} - -ul.searchresultspopup b { - color: blue; -} - -ul.searchresultspopup strong { - color: green; -} -/*Live Search styles end*/ diff --git a/src/main/webapp/new/index.html b/src/main/webapp/new/index.html deleted file mode 100644 index e6a24cbd..00000000 --- a/src/main/webapp/new/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - ИИ - - - - - - - - - - - - - - - - - - -
    - - - diff --git a/src/main/webapp/new/js/app.js b/src/main/webapp/new/js/app.js deleted file mode 100644 index 915bc0a5..00000000 --- a/src/main/webapp/new/js/app.js +++ /dev/null @@ -1,218 +0,0 @@ -angular.module('app', ['ui.router', 'live-search']) - .config(function($locationProvider, $urlRouterProvider, $stateProvider) { -// $locationProvider.html5Mode(true).hashPrefix('!'); - - $urlRouterProvider.otherwise("/home"); - // -// // Now set up the states - $stateProvider - .state('home', { - url: "/home", - templateUrl: "partials/home.html", - controller: HomeController - }) - .state('search', { - url: "/search/:query", - templateUrl: "partials/search.html" - }) - .state('article', { - url: "/a/:id", - templateUrl: "partials/article.html" - }) - .state('item', { - url: "/item/:number", - templateUrl: "partials/item.html", - controller: ItemController - }) - .state('term', { - url: "/term/:name", - templateUrl: "partials/term.html", - controller: TermController -// onEnter: function($location, $stateParams, $log){ -// $log.info($stateParams) -// } - }); - }) - .factory("analytics", function(){ - return { - registerEmptyTerm: function(termName) { - if (ga) { - ga('send', 'event', 'no-data', termName); - } - } - } - }) - .factory("$api", function($rootScope, $state, $http, errorService, $q){ - var apiUrl = "http://ii.ayfaar.org/api/"; -// var apiUrl = "http://localhost:8081/"; - return { - post: function(url, data) { - var deferred = $q.defer(); - $http.post(apiUrl+url, data, { - headers: { 'Content-Type': undefined }, - transformRequest: function(data, getHeaders) { - var headers = getHeaders(); - headers[ "Content-type" ] = "application/x-www-form-urlencoded; charset=utf-8"; - return( serializePost( data ) ); - } - }) - .then(function(response){ - deferred.resolve(response.data) - },function(response){ - errorService.resolve(response.error); - deferred.reject(response); - }); - return deferred.promise; - }, - get: function(url, data) { - var deferred = $q.defer(); - $http.get(apiUrl+url+serializeGet(data)) - .then(function(response){ - deferred.resolve(response.data) - },function(response){ - errorService.resolve(response.error); - deferred.reject(response); - }); - return deferred.promise; - } - }; - function serializeGet(obj) { - var str = []; - for(var p in obj) { - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - } - return str.length ? "?"+str.join("&") : ""; - } - function serializePost( data ) { - // If this is not an object, defer to native stringification. - if ( ! angular.isObject( data ) ) { - return( ( data == null ) ? "" : data.toString() ); - } - - var buffer = []; - - // Serialize each key in the object. - for ( var name in data ) { - if ( ! data.hasOwnProperty( name ) ) { - continue; - } - var value = data[ name ]; - - buffer.push( - encodeURIComponent( name ) + - "=" + - encodeURIComponent( ( value == null ) ? "" : value ) - ); - } - - // Serialize the buffer and clean it up for transportation. - var source = buffer - .join( "&" ) - .replace( /%20/g, "+" ) - ; - return( source ); - } - }) - .factory("errorService", function($rootScope, $state){ - return { - validationError: function(message) { - return $ionicPopup.alert({ - title: 'Ошибка', - template: message - }); - }, - resolve: function(error) { - var message = "Неизвесная ошибка"; - if (error) { - message = error.message; - switch (error.code) { - case "USER_NOT_FOUND": - message = "Пользователь не найден"; - break; - case "PASSWORD_NOT_VALID": - message = "Пароль не верный"; - break; - case "BAD_CREDENTIALS": - message = "Не верные email и пароль"; - break; - case "EMAIL_DUPLICATION": - message = "Такой email уже зарегистрированн в системе"; - break; - } - } - alert(message); - /*return $ionicPopup.alert({ - title: 'Ошибка', - template: message - });*/ - } - }; - }) - .factory('entityService', function(){ - var service = { - getName: function (entity) { - switch (service.getType(entity)) { - case 'term': - return entity.uri.replace("ии:термин:", ""); - case 'item': - return entity.uri.replace("ии:пункт:", ""); - } - }, - getType: function(entity) { - if (entity.uri.indexOf("ии:термин:") === 0) { - return 'term' - } - if (entity.uri.indexOf("ии:пункт:") === 0) { - return 'item' - } - } - }; - return service; - }) - .directive('entity', function($state, entityService) { - return { - restrict: 'E', - replace: true, - transclude: true, - template: '', - compile : function(element, attr, linker) { - return function ($scope, $element, $attr) { - var entity = $scope[$attr.ngModel]; -// var uiSref = "term({name:'"+name+"'})"; -// $attr.$set('uiSref', uiSref); - $element.append(entityService.getName(entity)); - $element.bind('click', function() { - $state.go(entity) - }) - } - } - /*link: function(scope, element, attrs) { - var entity = scope[attrs.ngModel]; - var name = entity.uri.replace("ии:термин:", ""); - var uiSref = "term({name:'"+name+"'})"; - attrs.$set('uiSref', uiSref); - element.removeAttr('ng-transclude'); - element.append(name); - $compile(element)(scope); - }*/ - }; - }) - .run(function($state, entityService){ - var defStateGo = $state.go; - $state.go = function(to, params, options) { - if (to.hasOwnProperty('uri')) { - var uri = to.uri; - if (entityService.getType(to) == "term") { - defStateGo.bind($state)("term", {name: entityService.getName(to)}) - } - } else { - defStateGo.bind($state)(to, params, options) - } - } - }); - -Array.prototype.append = function(array){ - this.push.apply(this, array) -}; diff --git a/src/main/webapp/new/js/controllers/home.js b/src/main/webapp/new/js/controllers/home.js deleted file mode 100644 index 589ac50f..00000000 --- a/src/main/webapp/new/js/controllers/home.js +++ /dev/null @@ -1,13 +0,0 @@ -function HomeController($scope, $api, $log, $state) { - $scope.searchCallback = function() { - return $api.get("v2/search/suggestions/"+$scope.query) - }; - $scope.suggestionSelected = function(suggestion) { - $state.go("term", {name: suggestion}); - }; - $scope.search = function() { - if ($scope.query) { - $state.go("search", {query: $scope.query}); - } - }; -} diff --git a/src/main/webapp/new/js/controllers/item.js b/src/main/webapp/new/js/controllers/item.js deleted file mode 100644 index 1ecbcdeb..00000000 --- a/src/main/webapp/new/js/controllers/item.js +++ /dev/null @@ -1,3 +0,0 @@ -function ItemController($scope, $stateParams, $log) { - $scope.number = $stateParams.number; -} diff --git a/src/main/webapp/new/js/controllers/term.js b/src/main/webapp/new/js/controllers/term.js deleted file mode 100644 index 11c23226..00000000 --- a/src/main/webapp/new/js/controllers/term.js +++ /dev/null @@ -1,17 +0,0 @@ -function TermController($scope, $stateParams, $api, analytics) { - $scope.name = $stateParams.name; - $scope.loading = true; - $api.get('term/', {name: $stateParams.name}) - .then(function(data) { - $scope.found = true; - for(var p in data) { - $scope[p] = data[p]; - } - if (data && !data.description && !data.shortDescription && !data.quotes.length && !data.related.length) { - analytics.registerEmptyTerm(data.name); - } - }) - ['finally'](function(){ - $scope.loading = false; - }) -} diff --git a/src/main/webapp/new/js/live-search.js b/src/main/webapp/new/js/live-search.js deleted file mode 100644 index 6b9bca11..00000000 --- a/src/main/webapp/new/js/live-search.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; - -angular.module("live-search", ["ng"]) - .directive("liveSearch", ["$compile", "$timeout", "$rootScope", function ($compile, $timeout) { - return { - restrict: 'E', - replace: true, - scope: { - fetchData: '=', - selected: '=', - liveSearchSelect: '=?', - liveSearchItemTemplate: '@', - fetchDelay: '=?', - liveSearchMaxResultSize: '=?' - }, - template: "", - link: function (scope, element, attrs, controller) { - var timeout; - - scope.results = []; - scope.visible = false; - scope.selectedIndex = -1; - - scope.select = function (index) { - scope.selectedIndex = index; - scope.visible = false; - }; - - scope.isSelected = function (index) { - return (scope.selectedIndex === index); - }; - - scope.$watch("selectedIndex", function(newValue, oldValue) { - var item = scope.results[newValue]; - if(item) { - if(attrs.liveSearchSelect) { - element.val(item[attrs.liveSearchSelect]); - } - else { - element.val(item); - } - if (scope.selected) { - scope.selected.call(scope, item) - } - } - }); - - scope.$watch("visible", function(newValue, oldValue) { - if(newValue == false) { - return; - } - scope.width = element[0].clientWidth; - var offset = getPosition(element[0]); - scope.top = offset.y + element[0].clientHeight + 1; - scope.left = offset.x; - }); - - element[0].onkeydown = function (e) { - //keydown - if (e.keyCode == 40) { - if(scope.selectedIndex + 1 === scope.results.length) { - scope.selectedIndex = 0; - } - else { - scope.selectedIndex++; - } - } - //keyup - else if (e.keyCode == 38) { - if(scope.selectedIndex == 0) { - scope.selectedIndex = scope.results.length - 1; - } - else if(scope.selectedIndex == -1) { - scope.selectedIndex = 0; - } - else scope.selectedIndex--; - } - //keydown or keyup - if (e.keyCode == 13) { - scope.visible = false; - } - - //unmanaged code needs to force apply - scope.$apply(); - }; - - element[0].onkeyup = function (e) { - if (e.keyCode == 13 || e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40) { - return false; - } - var target = element; - // Set Timeout - $timeout.cancel(timeout); - // Set Search String - var vals = target.val().split(","); - var search_string = vals[vals.length - 1].trim(); - // Do Search - if (search_string.length < 3 || search_string.length > 9) { - scope.visible = false; - //unmanaged code needs to force apply - scope.$apply(); - return; - } - timeout = $timeout(function () { - var results = []; - var promise = scope.fetchData.call(null, { query: search_string }); - promise.then(function (dataArray) { - if (dataArray) { - results = dataArray.slice(0, (scope.liveSearchMaxResultSize || 20) - 1); - } - scope.visible = true; - }); - promise.finally(function() { - scope.selectedIndex = -1; - scope.results = results.filter(function(elem, pos) { - return results.indexOf(elem) == pos; - }); - }); - }, scope.fetchDelay || 100); - }; - - var getPosition = function (element) { - var xPosition = 0; - var yPosition = 0; - - while (element) { - xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft); - yPosition += (element.offsetTop - element.scrollTop + element.clientTop); - element = element.offsetParent; - } - return { x: xPosition, y: yPosition }; - }; - - var itemTemplate = element.attr("live-search-item-template") || "{{result}}"; - var template = "
    • " + itemTemplate + "
    "; - var searchPopup = $compile(template)(scope); - document.body.appendChild(searchPopup[0]); - } - }; -}]); \ No newline at end of file diff --git a/src/main/webapp/new/lib/angular-ui-router.js b/src/main/webapp/new/lib/angular-ui-router.js deleted file mode 100644 index 8d10584a..00000000 --- a/src/main/webapp/new/lib/angular-ui-router.js +++ /dev/null @@ -1,3223 +0,0 @@ -/** - * State-based routing for AngularJS - * @version v0.2.10 - * @link http://angular-ui.github.com/ - * @license MIT License, http://www.opensource.org/licenses/MIT - */ - -/* commonjs package manager support (eg componentjs) */ -if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ - module.exports = 'ui.router'; -} - -(function (window, angular, undefined) { -/*jshint globalstrict:true*/ -/*global angular:false*/ -'use strict'; - -var isDefined = angular.isDefined, - isFunction = angular.isFunction, - isString = angular.isString, - isObject = angular.isObject, - isArray = angular.isArray, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy; - -function inherit(parent, extra) { - return extend(new (extend(function() {}, { prototype: parent }))(), extra); -} - -function merge(dst) { - forEach(arguments, function(obj) { - if (obj !== dst) { - forEach(obj, function(value, key) { - if (!dst.hasOwnProperty(key)) dst[key] = value; - }); - } - }); - return dst; -} - -/** - * Finds the common ancestor path between two states. - * - * @param {Object} first The first state. - * @param {Object} second The second state. - * @return {Array} Returns an array of state names in descending order, not including the root. - */ -function ancestors(first, second) { - var path = []; - - for (var n in first.path) { - if (first.path[n] !== second.path[n]) break; - path.push(first.path[n]); - } - return path; -} - -/** - * IE8-safe wrapper for `Object.keys()`. - * - * @param {Object} object A JavaScript object. - * @return {Array} Returns the keys of the object as an array. - */ -function keys(object) { - if (Object.keys) { - return Object.keys(object); - } - var result = []; - - angular.forEach(object, function(val, key) { - result.push(key); - }); - return result; -} - -/** - * IE8-safe wrapper for `Array.prototype.indexOf()`. - * - * @param {Array} array A JavaScript array. - * @param {*} value A value to search the array for. - * @return {Number} Returns the array index value of `value`, or `-1` if not present. - */ -function arraySearch(array, value) { - if (Array.prototype.indexOf) { - return array.indexOf(value, Number(arguments[2]) || 0); - } - var len = array.length >>> 0, from = Number(arguments[2]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - - if (from < 0) from += len; - - for (; from < len; from++) { - if (from in array && array[from] === value) return from; - } - return -1; -} - -/** - * Merges a set of parameters with all parameters inherited between the common parents of the - * current state and a given destination state. - * - * @param {Object} currentParams The value of the current state parameters ($stateParams). - * @param {Object} newParams The set of parameters which will be composited with inherited params. - * @param {Object} $current Internal definition of object representing the current state. - * @param {Object} $to Internal definition of object representing state to transition to. - */ -function inheritParams(currentParams, newParams, $current, $to) { - var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; - - for (var i in parents) { - if (!parents[i].params || !parents[i].params.length) continue; - parentParams = parents[i].params; - - for (var j in parentParams) { - if (arraySearch(inheritList, parentParams[j]) >= 0) continue; - inheritList.push(parentParams[j]); - inherited[parentParams[j]] = currentParams[parentParams[j]]; - } - } - return extend({}, inherited, newParams); -} - -/** - * Normalizes a set of values to string or `null`, filtering them by a list of keys. - * - * @param {Array} keys The list of keys to normalize/return. - * @param {Object} values An object hash of values to normalize. - * @return {Object} Returns an object hash of normalized string values. - */ -function normalize(keys, values) { - var normalized = {}; - - forEach(keys, function (name) { - var value = values[name]; - normalized[name] = (value != null) ? String(value) : null; - }); - return normalized; -} - -/** - * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. - * - * @param {Object} a The first object. - * @param {Object} b The second object. - * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, - * it defaults to the list of keys in `a`. - * @return {Boolean} Returns `true` if the keys match, otherwise `false`. - */ -function equalForKeys(a, b, keys) { - if (!keys) { - keys = []; - for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility - } - - for (var i=0; i - * - * - * - * - * - * - * - * - * - * - * - * - */ -angular.module('ui.router', ['ui.router.state']); - -angular.module('ui.router.compat', ['ui.router']); - -/** - * @ngdoc object - * @name ui.router.util.$resolve - * - * @requires $q - * @requires $injector - * - * @description - * Manages resolution of (acyclic) graphs of promises. - */ -$Resolve.$inject = ['$q', '$injector']; -function $Resolve( $q, $injector) { - - var VISIT_IN_PROGRESS = 1, - VISIT_DONE = 2, - NOTHING = {}, - NO_DEPENDENCIES = [], - NO_LOCALS = NOTHING, - NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); - - - /** - * @ngdoc function - * @name ui.router.util.$resolve#study - * @methodOf ui.router.util.$resolve - * - * @description - * Studies a set of invocables that are likely to be used multiple times. - *
    -   * $resolve.study(invocables)(locals, parent, self)
    -   * 
    - * is equivalent to - *
    -   * $resolve.resolve(invocables, locals, parent, self)
    -   * 
    - * but the former is more efficient (in fact `resolve` just calls `study` - * internally). - * - * @param {object} invocables Invocable objects - * @return {function} a function to pass in locals, parent and self - */ - this.study = function (invocables) { - if (!isObject(invocables)) throw new Error("'invocables' must be an object"); - - // Perform a topological sort of invocables to build an ordered plan - var plan = [], cycle = [], visited = {}; - function visit(value, key) { - if (visited[key] === VISIT_DONE) return; - - cycle.push(key); - if (visited[key] === VISIT_IN_PROGRESS) { - cycle.splice(0, cycle.indexOf(key)); - throw new Error("Cyclic dependency: " + cycle.join(" -> ")); - } - visited[key] = VISIT_IN_PROGRESS; - - if (isString(value)) { - plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); - } else { - var params = $injector.annotate(value); - forEach(params, function (param) { - if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); - }); - plan.push(key, value, params); - } - - cycle.pop(); - visited[key] = VISIT_DONE; - } - forEach(invocables, visit); - invocables = cycle = visited = null; // plan is all that's required - - function isResolve(value) { - return isObject(value) && value.then && value.$$promises; - } - - return function (locals, parent, self) { - if (isResolve(locals) && self === undefined) { - self = parent; parent = locals; locals = null; - } - if (!locals) locals = NO_LOCALS; - else if (!isObject(locals)) { - throw new Error("'locals' must be an object"); - } - if (!parent) parent = NO_PARENT; - else if (!isResolve(parent)) { - throw new Error("'parent' must be a promise returned by $resolve.resolve()"); - } - - // To complete the overall resolution, we have to wait for the parent - // promise and for the promise for each invokable in our plan. - var resolution = $q.defer(), - result = resolution.promise, - promises = result.$$promises = {}, - values = extend({}, locals), - wait = 1 + plan.length/3, - merged = false; - - function done() { - // Merge parent values we haven't got yet and publish our own $$values - if (!--wait) { - if (!merged) merge(values, parent.$$values); - result.$$values = values; - result.$$promises = true; // keep for isResolve() - resolution.resolve(values); - } - } - - function fail(reason) { - result.$$failure = reason; - resolution.reject(reason); - } - - // Short-circuit if parent has already failed - if (isDefined(parent.$$failure)) { - fail(parent.$$failure); - return result; - } - - // Merge parent values if the parent has already resolved, or merge - // parent promises and wait if the parent resolve is still in progress. - if (parent.$$values) { - merged = merge(values, parent.$$values); - done(); - } else { - extend(promises, parent.$$promises); - parent.then(done, fail); - } - - // Process each invocable in the plan, but ignore any where a local of the same name exists. - for (var i=0, ii=plan.length; i} The template html as a string, or a promise - * for that string. - */ - this.fromUrl = function (url, params) { - if (isFunction(url)) url = url(params); - if (url == null) return null; - else return $http - .get(url, { cache: $templateCache }) - .then(function(response) { return response.data; }); - }; - - /** - * @ngdoc function - * @name ui.router.util.$templateFactory#fromUrl - * @methodOf ui.router.util.$templateFactory - * - * @description - * Creates a template by invoking an injectable provider function. - * - * @param {Function} provider Function to invoke via `$injector.invoke` - * @param {Object} params Parameters for the template. - * @param {Object} locals Locals to pass to `invoke`. Defaults to - * `{ params: params }`. - * @return {string|Promise.} The template html as a string, or a promise - * for that string. - */ - this.fromProvider = function (provider, params, locals) { - return $injector.invoke(provider, null, locals || { params: params }); - }; -} - -angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); - -/** - * @ngdoc object - * @name ui.router.util.type:UrlMatcher - * - * @description - * Matches URLs against patterns and extracts named parameters from the path or the search - * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list - * of search parameters. Multiple search parameter names are separated by '&'. Search parameters - * do not influence whether or not a URL is matched, but their values are passed through into - * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. - * - * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace - * syntax, which optionally allows a regular expression for the parameter to be specified: - * - * * `':'` name - colon placeholder - * * `'*'` name - catch-all placeholder - * * `'{' name '}'` - curly placeholder - * * `'{' name ':' regexp '}'` - curly placeholder with regexp. Should the regexp itself contain - * curly braces, they must be in matched pairs or escaped with a backslash. - * - * Parameter names may contain only word characters (latin letters, digits, and underscore) and - * must be unique within the pattern (across both path and search parameters). For colon - * placeholders or curly placeholders without an explicit regexp, a path parameter matches any - * number of characters other than '/'. For catch-all placeholders the path parameter matches - * any number of characters. - * - * Examples: - * - * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for - * trailing slashes, and patterns have to match the entire path, not just a prefix. - * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or - * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. - * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. - * * `'/user/{id:[^/]*}'` - Same as the previous example. - * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id - * parameter consists of 1 to 8 hex digits. - * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the - * path into the parameter 'path'. - * * `'/files/*path'` - ditto. - * - * @param {string} pattern the pattern to compile into a matcher. - * - * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any - * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns - * non-null) will start with this prefix. - * - * @property {string} source The pattern that was passed into the contructor - * - * @property {string} sourcePath The path portion of the source property - * - * @property {string} sourceSearch The search portion of the source property - * - * @property {string} regex The constructed regex that will be used to match against the url when - * it is time to determine which url will match. - * - * @returns {Object} New UrlMatcher object - */ -function UrlMatcher(pattern) { - - // Find all placeholders and create a compiled pattern, using either classic or curly syntax: - // '*' name - // ':' name - // '{' name '}' - // '{' name ':' regexp '}' - // The regular expression is somewhat complicated due to the need to allow curly braces - // inside the regular expression. The placeholder regexp breaks down as follows: - // ([:*])(\w+) classic placeholder ($1 / $2) - // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4) - // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either - // [^{}\\]+ - anything other than curly braces or backslash - // \\. - a backslash escape - // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms - var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - names = {}, compiled = '^', last = 0, m, - segments = this.segments = [], - params = this.params = []; - - function addParameter(id) { - if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); - if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); - names[id] = true; - params.push(id); - } - - function quoteRegExp(string) { - return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); - } - - this.source = pattern; - - // Split into static segments separated by path parameter placeholders. - // The number of segments is always 1 more than the number of parameters. - var id, regexp, segment; - while ((m = placeholder.exec(pattern))) { - id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null - regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*'); - segment = pattern.substring(last, m.index); - if (segment.indexOf('?') >= 0) break; // we're into the search part - compiled += quoteRegExp(segment) + '(' + regexp + ')'; - addParameter(id); - segments.push(segment); - last = placeholder.lastIndex; - } - segment = pattern.substring(last); - - // Find any search parameter names and remove them from the last segment - var i = segment.indexOf('?'); - if (i >= 0) { - var search = this.sourceSearch = segment.substring(i); - segment = segment.substring(0, i); - this.sourcePath = pattern.substring(0, last+i); - - // Allow parameters to be separated by '?' as well as '&' to make concat() easier - forEach(search.substring(1).split(/[&?]/), addParameter); - } else { - this.sourcePath = pattern; - this.sourceSearch = ''; - } - - compiled += quoteRegExp(segment) + '$'; - segments.push(segment); - this.regexp = new RegExp(compiled); - this.prefix = segments[0]; -} - -/** - * @ngdoc function - * @name ui.router.util.type:UrlMatcher#concat - * @methodOf ui.router.util.type:UrlMatcher - * - * @description - * Returns a new matcher for a pattern constructed by appending the path part and adding the - * search parameters of the specified pattern to this pattern. The current pattern is not - * modified. This can be understood as creating a pattern for URLs that are relative to (or - * suffixes of) the current pattern. - * - * @example - * The following two matchers are equivalent: - * ``` - * new UrlMatcher('/user/{id}?q').concat('/details?date'); - * new UrlMatcher('/user/{id}/details?q&date'); - * ``` - * - * @param {string} pattern The pattern to append. - * @returns {ui.router.util.type:UrlMatcher} A matcher for the concatenated pattern. - */ -UrlMatcher.prototype.concat = function (pattern) { - // Because order of search parameters is irrelevant, we can add our own search - // parameters to the end of the new pattern. Parse the new pattern by itself - // and then join the bits together, but it's much easier to do this on a string level. - return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch); -}; - -UrlMatcher.prototype.toString = function () { - return this.source; -}; - -/** - * @ngdoc function - * @name ui.router.util.type:UrlMatcher#exec - * @methodOf ui.router.util.type:UrlMatcher - * - * @description - * Tests the specified path against this matcher, and returns an object containing the captured - * parameter values, or null if the path does not match. The returned object contains the values - * of any search parameters that are mentioned in the pattern, but their value may be null if - * they are not present in `searchParams`. This means that search parameters are always treated - * as optional. - * - * @example - * ``` - * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { x:'1', q:'hello' }); - * // returns { id:'bob', q:'hello', r:null } - * ``` - * - * @param {string} path The URL path to match, e.g. `$location.path()`. - * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. - * @returns {Object} The captured parameter values. - */ -UrlMatcher.prototype.exec = function (path, searchParams) { - var m = this.regexp.exec(path); - if (!m) return null; - - var params = this.params, nTotal = params.length, - nPath = this.segments.length-1, - values = {}, i; - - if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); - - for (i=0; i} An array of parameter names. Must be treated as read-only. If the - * pattern has no parameters, an empty array is returned. - */ -UrlMatcher.prototype.parameters = function () { - return this.params; -}; - -/** - * @ngdoc function - * @name ui.router.util.type:UrlMatcher#format - * @methodOf ui.router.util.type:UrlMatcher - * - * @description - * Creates a URL that matches this pattern by substituting the specified values - * for the path and search parameters. Null values for path parameters are - * treated as empty strings. - * - * @example - * ``` - * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' }); - * // returns '/user/bob?q=yes' - * ``` - * - * @param {Object} values the values to substitute for the parameters in this pattern. - * @returns {string} the formatted URL (path and optionally search part). - */ -UrlMatcher.prototype.format = function (values) { - var segments = this.segments, params = this.params; - if (!values) return segments.join(''); - - var nPath = segments.length-1, nTotal = params.length, - result = segments[0], i, search, value; - - for (i=0; i - * var app = angular.module('app', ['ui.router.router']); - * - * app.config(function ($urlRouterProvider) { - * // Here's an example of how you might allow case insensitive urls - * $urlRouterProvider.rule(function ($injector, $location) { - * var path = $location.path(), - * normalized = path.toLowerCase(); - * - * if (path !== normalized) { - * return normalized; - * } - * }); - * }); - * - * - * @param {object} rule Handler function that takes `$injector` and `$location` - * services as arguments. You can use them to return a valid path as a string. - * - * @return {object} $urlRouterProvider - $urlRouterProvider instance - */ - this.rule = - function (rule) { - if (!isFunction(rule)) throw new Error("'rule' must be a function"); - rules.push(rule); - return this; - }; - - /** - * @ngdoc object - * @name ui.router.router.$urlRouterProvider#otherwise - * @methodOf ui.router.router.$urlRouterProvider - * - * @description - * Defines a path that is used when an invalied route is requested. - * - * @example - *
    -   * var app = angular.module('app', ['ui.router.router']);
    -   *
    -   * app.config(function ($urlRouterProvider) {
    -   *   // if the path doesn't match any of the urls you configured
    -   *   // otherwise will take care of routing the user to the
    -   *   // specified url
    -   *   $urlRouterProvider.otherwise('/index');
    -   *
    -   *   // Example of using function rule as param
    -   *   $urlRouterProvider.otherwise(function ($injector, $location) {
    -   *     ...
    -   *   });
    -   * });
    -   * 
    - * - * @param {string|object} rule The url path you want to redirect to or a function - * rule that returns the url path. The function version is passed two params: - * `$injector` and `$location` services. - * - * @return {object} $urlRouterProvider - $urlRouterProvider instance - */ - this.otherwise = - function (rule) { - if (isString(rule)) { - var redirect = rule; - rule = function () { return redirect; }; - } - else if (!isFunction(rule)) throw new Error("'rule' must be a function"); - otherwise = rule; - return this; - }; - - - function handleIfMatch($injector, handler, match) { - if (!match) return false; - var result = $injector.invoke(handler, handler, { $match: match }); - return isDefined(result) ? result : true; - } - - /** - * @ngdoc function - * @name ui.router.router.$urlRouterProvider#when - * @methodOf ui.router.router.$urlRouterProvider - * - * @description - * Registers a handler for a given url matching. if handle is a string, it is - * treated as a redirect, and is interpolated according to the syyntax of match - * (i.e. like String.replace() for RegExp, or like a UrlMatcher pattern otherwise). - * - * If the handler is a function, it is injectable. It gets invoked if `$location` - * matches. You have the option of inject the match object as `$match`. - * - * The handler can return - * - * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` - * will continue trying to find another one that matches. - * - **string** which is treated as a redirect and passed to `$location.url()` - * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. - * - * @example - *
    -   * var app = angular.module('app', ['ui.router.router']);
    -   *
    -   * app.config(function ($urlRouterProvider) {
    -   *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
    -   *     if ($state.$current.navigable !== state ||
    -   *         !equalForKeys($match, $stateParams) {
    -   *      $state.transitionTo(state, $match, false);
    -   *     }
    -   *   });
    -   * });
    -   * 
    - * - * @param {string|object} what The incoming path that you want to redirect. - * @param {string|object} handler The path you want to redirect your user to. - */ - this.when = - function (what, handler) { - var redirect, handlerIsString = isString(handler); - if (isString(what)) what = $urlMatcherFactory.compile(what); - - if (!handlerIsString && !isFunction(handler) && !isArray(handler)) - throw new Error("invalid 'handler' in when()"); - - var strategies = { - matcher: function (what, handler) { - if (handlerIsString) { - redirect = $urlMatcherFactory.compile(handler); - handler = ['$match', function ($match) { return redirect.format($match); }]; - } - return extend(function ($injector, $location) { - return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); - }, { - prefix: isString(what.prefix) ? what.prefix : '' - }); - }, - regex: function (what, handler) { - if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); - - if (handlerIsString) { - redirect = handler; - handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; - } - return extend(function ($injector, $location) { - return handleIfMatch($injector, handler, what.exec($location.path())); - }, { - prefix: regExpPrefix(what) - }); - } - }; - - var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; - - for (var n in check) { - if (check[n]) { - return this.rule(strategies[n](what, handler)); - } - } - - throw new Error("invalid 'what' in when()"); - }; - - /** - * @ngdoc object - * @name ui.router.router.$urlRouter - * - * @requires $location - * @requires $rootScope - * @requires $injector - * - * @description - * - */ - this.$get = - [ '$location', '$rootScope', '$injector', - function ($location, $rootScope, $injector) { - // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree - function update(evt) { - if (evt && evt.defaultPrevented) return; - function check(rule) { - var handled = rule($injector, $location); - if (handled) { - if (isString(handled)) $location.replace().url(handled); - return true; - } - return false; - } - var n=rules.length, i; - for (i=0; i - * angular.module('app', ['ui.router']); - * .run(function($rootScope, $urlRouter) { - * $rootScope.$on('$locationChangeSuccess', function(evt) { - * // Halt state change from even starting - * evt.preventDefault(); - * // Perform custom logic - * var meetsRequirement = ... - * // Continue with the update and state transition if logic allows - * if (meetsRequirement) $urlRouter.sync(); - * }); - * }); - * - */ - sync: function () { - update(); - } - }; - }]; -} - -angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); - -/** - * @ngdoc object - * @name ui.router.state.$stateProvider - * - * @requires ui.router.router.$urlRouterProvider - * @requires ui.router.util.$urlMatcherFactoryProvider - * @requires $locationProvider - * - * @description - * The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely - * on state. - * - * A state corresponds to a "place" in the application in terms of the overall UI and - * navigation. A state describes (via the controller / template / view properties) what - * the UI looks like and does at that place. - * - * States often have things in common, and the primary way of factoring out these - * commonalities in this model is via the state hierarchy, i.e. parent/child states aka - * nested states. - * - * The `$stateProvider` provides interfaces to declare these states for your app. - */ -$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider', '$locationProvider']; -function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $locationProvider) { - - var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; - - // Builds state properties from definition passed to registerState() - var stateBuilder = { - - // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. - // state.children = []; - // if (parent) parent.children.push(state); - parent: function(state) { - if (isDefined(state.parent) && state.parent) return findState(state.parent); - // regex matches any valid composite state name - // would match "contact.list" but not "contacts" - var compositeName = /^(.+)\.[^.]+$/.exec(state.name); - return compositeName ? findState(compositeName[1]) : root; - }, - - // inherit 'data' from parent and override by own values (if any) - data: function(state) { - if (state.parent && state.parent.data) { - state.data = state.self.data = extend({}, state.parent.data, state.data); - } - return state.data; - }, - - // Build a URLMatcher if necessary, either via a relative or absolute URL - url: function(state) { - var url = state.url; - - if (isString(url)) { - if (url.charAt(0) == '^') { - return $urlMatcherFactory.compile(url.substring(1)); - } - return (state.parent.navigable || root).url.concat(url); - } - - if ($urlMatcherFactory.isMatcher(url) || url == null) { - return url; - } - throw new Error("Invalid url '" + url + "' in state '" + state + "'"); - }, - - // Keep track of the closest ancestor state that has a URL (i.e. is navigable) - navigable: function(state) { - return state.url ? state : (state.parent ? state.parent.navigable : null); - }, - - // Derive parameters for this state and ensure they're a super-set of parent's parameters - params: function(state) { - if (!state.params) { - return state.url ? state.url.parameters() : state.parent.params; - } - if (!isArray(state.params)) throw new Error("Invalid params in state '" + state + "'"); - if (state.url) throw new Error("Both params and url specicified in state '" + state + "'"); - return state.params; - }, - - // If there is no explicit multi-view configuration, make one up so we don't have - // to handle both cases in the view directive later. Note that having an explicit - // 'views' property will mean the default unnamed view properties are ignored. This - // is also a good time to resolve view names to absolute names, so everything is a - // straight lookup at link time. - views: function(state) { - var views = {}; - - forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { - if (name.indexOf('@') < 0) name += '@' + state.parent.name; - views[name] = view; - }); - return views; - }, - - ownParams: function(state) { - if (!state.parent) { - return state.params; - } - var paramNames = {}; forEach(state.params, function (p) { paramNames[p] = true; }); - - forEach(state.parent.params, function (p) { - if (!paramNames[p]) { - throw new Error("Missing required parameter '" + p + "' in state '" + state.name + "'"); - } - paramNames[p] = false; - }); - var ownParams = []; - - forEach(paramNames, function (own, p) { - if (own) ownParams.push(p); - }); - return ownParams; - }, - - // Keep a full path from the root down to this state as this is needed for state activation. - path: function(state) { - return state.parent ? state.parent.path.concat(state) : []; // exclude root from path - }, - - // Speed up $state.contains() as it's used a lot - includes: function(state) { - var includes = state.parent ? extend({}, state.parent.includes) : {}; - includes[state.name] = true; - return includes; - }, - - $delegates: {} - }; - - function isRelative(stateName) { - return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; - } - - function findState(stateOrName, base) { - var isStr = isString(stateOrName), - name = isStr ? stateOrName : stateOrName.name, - path = isRelative(name); - - if (path) { - if (!base) throw new Error("No reference point given for path '" + name + "'"); - var rel = name.split("."), i = 0, pathLength = rel.length, current = base; - - for (; i < pathLength; i++) { - if (rel[i] === "" && i === 0) { - current = base; - continue; - } - if (rel[i] === "^") { - if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); - current = current.parent; - continue; - } - break; - } - rel = rel.slice(i).join("."); - name = current.name + (current.name && rel ? "." : "") + rel; - } - var state = states[name]; - - if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { - return state; - } - return undefined; - } - - function queueState(parentName, state) { - if (!queue[parentName]) { - queue[parentName] = []; - } - queue[parentName].push(state); - } - - function registerState(state) { - // Wrap a new object around the state so we can store our private details easily. - state = inherit(state, { - self: state, - resolve: state.resolve || {}, - toString: function() { return this.name; } - }); - - var name = state.name; - if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); - if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); - - // Get parent name - var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) - : (isString(state.parent)) ? state.parent - : ''; - - // If parent is not registered yet, add state to queue and register later - if (parentName && !states[parentName]) { - return queueState(parentName, state.self); - } - - for (var key in stateBuilder) { - if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); - } - states[name] = state; - - // Register the state in the global state list and with $urlRouter if necessary. - if (!state[abstractKey] && state.url) { - $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { - if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { - $state.transitionTo(state, $match, { location: false }); - } - }]); - } - - // Register any queued children - if (queue[name]) { - for (var i = 0; i < queue[name].length; i++) { - registerState(queue[name][i]); - } - } - - return state; - } - - // Checks text to see if it looks like a glob. - function isGlob (text) { - return text.indexOf('*') > -1; - } - - // Returns true if glob matches current $state name. - function doesStateMatchGlob (glob) { - var globSegments = glob.split('.'), - segments = $state.$current.name.split('.'); - - //match greedy starts - if (globSegments[0] === '**') { - segments = segments.slice(segments.indexOf(globSegments[1])); - segments.unshift('**'); - } - //match greedy ends - if (globSegments[globSegments.length - 1] === '**') { - segments.splice(segments.indexOf(globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); - segments.push('**'); - } - - if (globSegments.length != segments.length) { - return false; - } - - //match single stars - for (var i = 0, l = globSegments.length; i < l; i++) { - if (globSegments[i] === '*') { - segments[i] = '*'; - } - } - - return segments.join('') === globSegments.join(''); - } - - - // Implicit root state that is always active - root = registerState({ - name: '', - url: '^', - views: null, - 'abstract': true - }); - root.navigable = null; - - - /** - * @ngdoc function - * @name ui.router.state.$stateProvider#decorator - * @methodOf ui.router.state.$stateProvider - * - * @description - * Allows you to extend (carefully) or override (at your own peril) the - * `stateBuilder` object used internally by `$stateProvider`. This can be used - * to add custom functionality to ui-router, for example inferring templateUrl - * based on the state name. - * - * When passing only a name, it returns the current (original or decorated) builder - * function that matches `name`. - * - * The builder functions that can be decorated are listed below. Though not all - * necessarily have a good use case for decoration, that is up to you to decide. - * - * In addition, users can attach custom decorators, which will generate new - * properties within the state's internal definition. There is currently no clear - * use-case for this beyond accessing internal states (i.e. $state.$current), - * however, expect this to become increasingly relevant as we introduce additional - * meta-programming features. - * - * **Warning**: Decorators should not be interdependent because the order of - * execution of the builder functions in non-deterministic. Builder functions - * should only be dependent on the state definition object and super function. - * - * - * Existing builder functions and current return values: - * - * - **parent** `{object}` - returns the parent state object. - * - **data** `{object}` - returns state data, including any inherited data that is not - * overridden by own values (if any). - * - **url** `{object}` - returns a {link ui.router.util.type:UrlMatcher} or null. - * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is - * navigable). - * - **params** `{object}` - returns an array of state params that are ensured to - * be a super-set of parent's params. - * - **views** `{object}` - returns a views object where each key is an absolute view - * name (i.e. "viewName@stateName") and each value is the config object - * (template, controller) for the view. Even when you don't use the views object - * explicitly on a state config, one is still created for you internally. - * So by decorating this builder function you have access to decorating template - * and controller properties. - * - **ownParams** `{object}` - returns an array of params that belong to the state, - * not including any params defined by ancestor states. - * - **path** `{string}` - returns the full path from the root down to this state. - * Needed for state activation. - * - **includes** `{object}` - returns an object that includes every state that - * would pass a '$state.includes()' test. - * - * @example - *
    -   * // Override the internal 'views' builder with a function that takes the state
    -   * // definition, and a reference to the internal function being overridden:
    -   * $stateProvider.decorator('views', function ($state, parent) {
    -   *   var result = {},
    -   *       views = parent(state);
    -   *
    -   *   angular.forEach(view, function (config, name) {
    -   *     var autoName = (state.name + '.' + name).replace('.', '/');
    -   *     config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
    -   *     result[name] = config;
    -   *   });
    -   *   return result;
    -   * });
    -   *
    -   * $stateProvider.state('home', {
    -   *   views: {
    -   *     'contact.list': { controller: 'ListController' },
    -   *     'contact.item': { controller: 'ItemController' }
    -   *   }
    -   * });
    -   *
    -   * // ...
    -   *
    -   * $state.go('home');
    -   * // Auto-populates list and item views with /partials/home/contact/list.html,
    -   * // and /partials/home/contact/item.html, respectively.
    -   * 
    - * - * @param {string} name The name of the builder function to decorate. - * @param {object} func A function that is responsible for decorating the original - * builder function. The function receives two parameters: - * - * - `{object}` - state - The state config object. - * - `{object}` - super - The original builder function. - * - * @return {object} $stateProvider - $stateProvider instance - */ - this.decorator = decorator; - function decorator(name, func) { - /*jshint validthis: true */ - if (isString(name) && !isDefined(func)) { - return stateBuilder[name]; - } - if (!isFunction(func) || !isString(name)) { - return this; - } - if (stateBuilder[name] && !stateBuilder.$delegates[name]) { - stateBuilder.$delegates[name] = stateBuilder[name]; - } - stateBuilder[name] = func; - return this; - } - - /** - * @ngdoc function - * @name ui.router.state.$stateProvider#state - * @methodOf ui.router.state.$stateProvider - * - * @description - * Registers a state configuration under a given state name. The stateConfig object - * has the following acceptable properties. - * - * - * - * - **`template`** - {string|function=} - html template as a string or a function that returns - * an html template as a string which should be used by the uiView directives. This property - * takes precedence over templateUrl. - * - * If `template` is a function, it will be called with the following parameters: - * - * - {array.<object>} - state parameters extracted from the current $location.path() by - * applying the current state - * - * - * - * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html - * template that should be used by uiView. - * - * If `templateUrl` is a function, it will be called with the following parameters: - * - * - {array.<object>} - state parameters extracted from the current $location.path() by - * applying the current state - * - * - * - * - **`templateProvider`** - {function=} - Provider function that returns HTML content - * string. - * - * - * - * - **`controller`** - {string|function=} - Controller fn that should be associated with newly - * related scope or the name of a registered controller if passed as a string. - * - * - * - * - **`controllerProvider`** - {function=} - Injectable provider function that returns - * the actual controller or string. - * - * - * - * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be - * published to scope under the controllerAs name. - * - * - * - * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which - * should be injected into the controller. If any of these dependencies are promises, - * the router will wait for them all to be resolved or one to be rejected before the - * controller is instantiated. If all the promises are resolved successfully, the values - * of the resolved promises are injected and $stateChangeSuccess event is fired. If any - * of the promises are rejected the $stateChangeError event is fired. The map object is: - * - * - key - {string}: name of dependency to be injected into controller - * - factory - {string|function}: If string then it is alias for service. Otherwise if function, - * it is injected and return value it treated as dependency. If result is a promise, it is - * resolved before its value is injected into controller. - * - * - * - * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or - * transitioned to, the `$stateParams` service will be populated with any - * parameters that were passed. - * - * - * - * - **`params`** - {object=} - An array of parameter names or regular expressions. Only - * use this within a state if you are not using url. Otherwise you can specify your - * parameters within the url. When a state is navigated or transitioned to, the - * $stateParams service will be populated with any parameters that were passed. - * - * - * - * - **`views`** - {object=} - Use the views property to set up multiple views or to target views - * manually/explicitly. - * - * - * - * - **`abstract`** - {boolean=} - An abstract state will never be directly activated, - * but can provide inherited properties to its common children states. - * - * - * - * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way - * to trigger an action or dispatch an event, such as opening a dialog. - * - * - * - * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to - * trigger an action or dispatch an event, such as opening a dialog. - * - * - * - * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state - * just because a search/query parameter has changed (via $location.search() or $location.hash()). - * Useful for when you'd like to modify $location.search() without triggering a reload. - * - * - * - * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration. - * - * @example - *
    -   * // Some state name examples
    -   *
    -   * // stateName can be a single top-level name (must be unique).
    -   * $stateProvider.state("home", {});
    -   *
    -   * // Or it can be a nested state name. This state is a child of the 
    -   * // above "home" state.
    -   * $stateProvider.state("home.newest", {});
    -   *
    -   * // Nest states as deeply as needed.
    -   * $stateProvider.state("home.newest.abc.xyz.inception", {});
    -   *
    -   * // state() returns $stateProvider, so you can chain state declarations.
    -   * $stateProvider
    -   *   .state("home", {})
    -   *   .state("about", {})
    -   *   .state("contacts", {});
    -   * 
    - * - * @param {string} name A unique state name, e.g. "home", "about", "contacts". - * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". - * @param {object} definition State configuration object. - */ - this.state = state; - function state(name, definition) { - /*jshint validthis: true */ - if (isObject(name)) definition = name; - else definition.name = name; - registerState(definition); - return this; - } - - /** - * @ngdoc object - * @name ui.router.state.$state - * - * @requires $rootScope - * @requires $q - * @requires ui.router.state.$view - * @requires $injector - * @requires ui.router.util.$resolve - * @requires ui.router.state.$stateParams - * - * @property {object} params A param object, e.g. {sectionId: section.id)}, that - * you'd like to test against the current active state. - * @property {object} current A reference to the state's config object. However - * you passed it in. Useful for accessing custom data. - * @property {object} transition Currently pending transition. A promise that'll - * resolve or reject. - * - * @description - * `$state` service is responsible for representing states as well as transitioning - * between them. It also provides interfaces to ask for current state or even states - * you're coming from. - */ - // $urlRouter is injected just to ensure it gets instantiated - this.$get = $get; - $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$location', '$urlRouter', '$browser']; - function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $location, $urlRouter, $browser) { - - var TransitionSuperseded = $q.reject(new Error('transition superseded')); - var TransitionPrevented = $q.reject(new Error('transition prevented')); - var TransitionAborted = $q.reject(new Error('transition aborted')); - var TransitionFailed = $q.reject(new Error('transition failed')); - var currentLocation = $location.url(); - var baseHref = $browser.baseHref(); - - function syncUrl() { - if ($location.url() !== currentLocation) { - $location.url(currentLocation); - $location.replace(); - } - } - - root.locals = { resolve: null, globals: { $stateParams: {} } }; - $state = { - params: {}, - current: root.self, - $current: root, - transition: null - }; - - /** - * @ngdoc function - * @name ui.router.state.$state#reload - * @methodOf ui.router.state.$state - * - * @description - * A method that force reloads the current state. All resolves are re-resolved, events are not re-fired, - * and controllers reinstantiated (bug with controllers reinstantiating right now, fixing soon). - * - * @example - *
    -     * var app angular.module('app', ['ui.router']);
    -     *
    -     * app.controller('ctrl', function ($scope, $state) {
    -     *   $scope.reload = function(){
    -     *     $state.reload();
    -     *   }
    -     * });
    -     * 
    - * - * `reload()` is just an alias for: - *
    -     * $state.transitionTo($state.current, $stateParams, { 
    -     *   reload: true, inherit: false, notify: false 
    -     * });
    -     * 
    - */ - $state.reload = function reload() { - $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false }); - }; - - /** - * @ngdoc function - * @name ui.router.state.$state#go - * @methodOf ui.router.state.$state - * - * @description - * Convenience method for transitioning to a new state. `$state.go` calls - * `$state.transitionTo` internally but automatically sets options to - * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. - * This allows you to easily use an absolute or relative to path and specify - * only the parameters you'd like to update (while letting unspecified parameters - * inherit from the currently active ancestor states). - * - * @example - *
    -     * var app = angular.module('app', ['ui.router']);
    -     *
    -     * app.controller('ctrl', function ($scope, $state) {
    -     *   $scope.changeState = function () {
    -     *     $state.go('contact.detail');
    -     *   };
    -     * });
    -     * 
    - * - * - * @param {string} to Absolute state name or relative state path. Some examples: - * - * - `$state.go('contact.detail')` - will go to the `contact.detail` state - * - `$state.go('^')` - will go to a parent state - * - `$state.go('^.sibling')` - will go to a sibling state - * - `$state.go('.child.grandchild')` - will go to grandchild state - * - * @param {object=} params A map of the parameters that will be sent to the state, - * will populate $stateParams. Any parameters that are not specified will be inherited from currently - * defined parameters. This allows, for example, going to a sibling state that shares parameters - * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. - * transitioning to a sibling will get you the parameters for all parents, transitioning to a child - * will get you all current parameters, etc. - * @param {object=} options Options object. The options are: - * - * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` - * will not. If string, must be `"replace"`, which will update url and also replace last history record. - * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), - * defines which state to be relative from. - * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. - * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params - * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd - * use this when you want to force a reload when *everything* is the same, including search params. - * - * @returns {promise} A promise representing the state of the new transition. - * - * Possible success values: - * - * - $state.current - * - *
    Possible rejection values: - * - * - 'transition superseded' - when a newer transition has been started after this one - * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener - * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or - * when a `$stateNotFound` `event.retry` promise errors. - * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. - * - *resolve error* - when an error has occurred with a `resolve` - * - */ - $state.go = function go(to, params, options) { - return this.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); - }; - - /** - * @ngdoc function - * @name ui.router.state.$state#transitionTo - * @methodOf ui.router.state.$state - * - * @description - * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} - * uses `transitionTo` internally. `$state.go` is recommended in most situations. - * - * @example - *
    -     * var app = angular.module('app', ['ui.router']);
    -     *
    -     * app.controller('ctrl', function ($scope, $state) {
    -     *   $scope.changeState = function () {
    -     *     $state.transitionTo('contact.detail');
    -     *   };
    -     * });
    -     * 
    - * - * @param {string} to State name. - * @param {object=} toParams A map of the parameters that will be sent to the state, - * will populate $stateParams. - * @param {object=} options Options object. The options are: - * - * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` - * will not. If string, must be `"replace"`, which will update url and also replace last history record. - * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), - * defines which state to be relative from. - * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. - * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params - * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd - * use this when you want to force a reload when *everything* is the same, including search params. - * - * @returns {promise} A promise representing the state of the new transition. See - * {@link ui.router.state.$state#methods_go $state.go}. - */ - $state.transitionTo = function transitionTo(to, toParams, options) { - toParams = toParams || {}; - options = extend({ - location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false - }, options || {}); - - var from = $state.$current, fromParams = $state.params, fromPath = from.path; - var evt, toState = findState(to, options.relative); - - if (!isDefined(toState)) { - // Broadcast not found event and abort the transition if prevented - var redirect = { to: to, toParams: toParams, options: options }; - - /** - * @ngdoc event - * @name ui.router.state.$state#$stateNotFound - * @eventOf ui.router.state.$state - * @eventType broadcast on root scope - * @description - * Fired when a requested state **cannot be found** using the provided state name during transition. - * The event is broadcast allowing any handlers a single chance to deal with the error (usually by - * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, - * you can see its three properties in the example. You can use `event.preventDefault()` to abort the - * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. - * - * @param {Object} event Event object. - * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. - * @param {State} fromState Current state object. - * @param {Object} fromParams Current state params. - * - * @example - * - *
    -         * // somewhere, assume lazy.state has not been defined
    -         * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
    -         *
    -         * // somewhere else
    -         * $scope.$on('$stateNotFound',
    -         * function(event, unfoundState, fromState, fromParams){
    -         *     console.log(unfoundState.to); // "lazy.state"
    -         *     console.log(unfoundState.toParams); // {a:1, b:2}
    -         *     console.log(unfoundState.options); // {inherit:false} + default options
    -         * })
    -         * 
    - */ - evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams); - if (evt.defaultPrevented) { - syncUrl(); - return TransitionAborted; - } - - // Allow the handler to return a promise to defer state lookup retry - if (evt.retry) { - if (options.$retry) { - syncUrl(); - return TransitionFailed; - } - var retryTransition = $state.transition = $q.when(evt.retry); - retryTransition.then(function() { - if (retryTransition !== $state.transition) return TransitionSuperseded; - redirect.options.$retry = true; - return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); - }, function() { - return TransitionAborted; - }); - syncUrl(); - return retryTransition; - } - - // Always retry once if the $stateNotFound was not prevented - // (handles either redirect changed or state lazy-definition) - to = redirect.to; - toParams = redirect.toParams; - options = redirect.options; - toState = findState(to, options.relative); - if (!isDefined(toState)) { - if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); - throw new Error("No such state '" + to + "'"); - } - } - if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); - if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); - to = toState; - - var toPath = to.path; - - // Starting from the root of the path, keep all levels that haven't changed - var keep, state, locals = root.locals, toLocals = []; - for (keep = 0, state = toPath[keep]; - state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams) && !options.reload; - keep++, state = toPath[keep]) { - locals = toLocals[keep] = state.locals; - } - - // If we're going to the same state and all locals are kept, we've got nothing to do. - // But clear 'transition', as we still want to cancel any other pending transitions. - // TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves, - // because we might accidentally abort a legitimate transition initiated from code? - if (shouldTriggerReload(to, from, locals, options) ) { - if ( to.self.reloadOnSearch !== false ) - syncUrl(); - $state.transition = null; - return $q.when($state.current); - } - - // Normalize/filter parameters before we pass them to event handlers etc. - toParams = normalize(to.params, toParams || {}); - - // Broadcast start event and cancel the transition if requested - if (options.notify) { - /** - * @ngdoc event - * @name ui.router.state.$state#$stateChangeStart - * @eventOf ui.router.state.$state - * @eventType broadcast on root scope - * @description - * Fired when the state transition **begins**. You can use `event.preventDefault()` - * to prevent the transition from happening and then the transition promise will be - * rejected with a `'transition prevented'` value. - * - * @param {Object} event Event object. - * @param {State} toState The state being transitioned to. - * @param {Object} toParams The params supplied to the `toState`. - * @param {State} fromState The current state, pre-transition. - * @param {Object} fromParams The params supplied to the `fromState`. - * - * @example - * - *
    -         * $rootScope.$on('$stateChangeStart',
    -         * function(event, toState, toParams, fromState, fromParams){
    -         *     event.preventDefault();
    -         *     // transitionTo() promise will be rejected with
    -         *     // a 'transition prevented' error
    -         * })
    -         * 
    - */ - evt = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams); - if (evt.defaultPrevented) { - syncUrl(); - return TransitionPrevented; - } - } - - // Resolve locals for the remaining states, but don't update any global state just - // yet -- if anything fails to resolve the current state needs to remain untouched. - // We also set up an inheritance chain for the locals here. This allows the view directive - // to quickly look up the correct definition for each view in the current state. Even - // though we create the locals object itself outside resolveState(), it is initially - // empty and gets filled asynchronously. We need to keep track of the promise for the - // (fully resolved) current locals, and pass this down the chain. - var resolved = $q.when(locals); - for (var l=keep; l=keep; l--) { - exiting = fromPath[l]; - if (exiting.self.onExit) { - $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); - } - exiting.locals = null; - } - - // Enter 'to' states not kept - for (l=keep; l - * $state.is('contact.details.item'); // returns true - * $state.is(contactDetailItemStateObject); // returns true - * - * // everything else would return false - * - * - * @param {string|object} stateName The state name or state object you'd like to check. - * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like - * to test against the current active state. - * @returns {boolean} Returns true if it is the state. - */ - $state.is = function is(stateOrName, params) { - var state = findState(stateOrName); - - if (!isDefined(state)) { - return undefined; - } - - if ($state.$current !== state) { - return false; - } - - return isDefined(params) && params !== null ? angular.equals($stateParams, params) : true; - }; - - /** - * @ngdoc function - * @name ui.router.state.$state#includes - * @methodOf ui.router.state.$state - * - * @description - * A method to determine if the current active state is equal to or is the child of the - * state stateName. If any params are passed then they will be tested for a match as well. - * Not all the parameters need to be passed, just the ones you'd like to test for equality. - * - * @example - *
    -     * $state.$current.name = 'contacts.details.item';
    -     *
    -     * $state.includes("contacts"); // returns true
    -     * $state.includes("contacts.details"); // returns true
    -     * $state.includes("contacts.details.item"); // returns true
    -     * $state.includes("contacts.list"); // returns false
    -     * $state.includes("about"); // returns false
    -     * 
    - * - * @description - * Basic globing patterns will also work. - * - * @example - *
    -     * $state.$current.name = 'contacts.details.item.url';
    -     *
    -     * $state.includes("*.details.*.*"); // returns true
    -     * $state.includes("*.details.**"); // returns true
    -     * $state.includes("**.item.**"); // returns true
    -     * $state.includes("*.details.item.url"); // returns true
    -     * $state.includes("*.details.*.url"); // returns true
    -     * $state.includes("*.details.*"); // returns false
    -     * $state.includes("item.**"); // returns false
    -     * 
    - * - * @param {string} stateOrName A partial name to be searched for within the current state name. - * @param {object} params A param object, e.g. `{sectionId: section.id}`, - * that you'd like to test against the current active state. - * @returns {boolean} Returns true if it does include the state - */ - - $state.includes = function includes(stateOrName, params) { - if (isString(stateOrName) && isGlob(stateOrName)) { - if (doesStateMatchGlob(stateOrName)) { - stateOrName = $state.$current.name; - } else { - return false; - } - } - - var state = findState(stateOrName); - if (!isDefined(state)) { - return undefined; - } - - if (!isDefined($state.$current.includes[state.name])) { - return false; - } - - var validParams = true; - angular.forEach(params, function(value, key) { - if (!isDefined($stateParams[key]) || $stateParams[key] !== value) { - validParams = false; - } - }); - return validParams; - }; - - - /** - * @ngdoc function - * @name ui.router.state.$state#href - * @methodOf ui.router.state.$state - * - * @description - * A url generation method that returns the compiled url for the given state populated with the given params. - * - * @example - *
    -     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
    -     * 
    - * - * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. - * @param {object=} params An object of parameter values to fill the state's required parameters. - * @param {object=} options Options object. The options are: - * - * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the - * first parameter, then the constructed href url will be built from the first navigable ancestor (aka - * ancestor with a valid url). - * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), - * defines which state to be relative from. - * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". - * - * @returns {string} compiled state url - */ - $state.href = function href(stateOrName, params, options) { - options = extend({ lossy: true, inherit: false, absolute: false, relative: $state.$current }, options || {}); - var state = findState(stateOrName, options.relative); - if (!isDefined(state)) return null; - - params = inheritParams($stateParams, params || {}, $state.$current, state); - var nav = (state && options.lossy) ? state.navigable : state; - var url = (nav && nav.url) ? nav.url.format(normalize(state.params, params || {})) : null; - if (!$locationProvider.html5Mode() && url) { - url = "#" + $locationProvider.hashPrefix() + url; - } - - if (baseHref !== '/') { - if ($locationProvider.html5Mode()) { - url = baseHref.slice(0, -1) + url; - } else if (options.absolute){ - url = baseHref.slice(1) + url; - } - } - - if (options.absolute && url) { - url = $location.protocol() + '://' + - $location.host() + - ($location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port()) + - (!$locationProvider.html5Mode() && url ? '/' : '') + - url; - } - return url; - }; - - /** - * @ngdoc function - * @name ui.router.state.$state#get - * @methodOf ui.router.state.$state - * - * @description - * Returns the state configuration object for any specific state or all states. - * - * @param {string|object=} stateOrName If provided, will only get the config for - * the requested state. If not provided, returns an array of ALL state configs. - * @returns {object|array} State configuration object or array of all objects. - */ - $state.get = function (stateOrName, context) { - if (!isDefined(stateOrName)) { - var list = []; - forEach(states, function(state) { list.push(state.self); }); - return list; - } - var state = findState(stateOrName, context); - return (state && state.self) ? state.self : null; - }; - - function resolveState(state, params, paramsAreFiltered, inherited, dst) { - // Make a restricted $stateParams with only the parameters that apply to this state if - // necessary. In addition to being available to the controller and onEnter/onExit callbacks, - // we also need $stateParams to be available for any $injector calls we make during the - // dependency resolution process. - var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params, params); - var locals = { $stateParams: $stateParams }; - - // Resolve 'global' dependencies for the state, i.e. those not specific to a view. - // We're also including $stateParams in this; that way the parameters are restricted - // to the set that should be visible to the state, and are independent of when we update - // the global $state and $stateParams values. - dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); - var promises = [ dst.resolve.then(function (globals) { - dst.globals = globals; - }) ]; - if (inherited) promises.push(inherited); - - // Resolve template and dependencies for all views. - forEach(state.views, function (view, name) { - var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); - injectables.$template = [ function () { - return $view.load(name, { view: view, locals: locals, params: $stateParams, notify: false }) || ''; - }]; - - promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { - // References to the controller (only instantiated at link time) - if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { - var injectLocals = angular.extend({}, injectables, locals); - result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); - } else { - result.$$controller = view.controller; - } - // Provide access to the state itself for internal use - result.$$state = state; - result.$$controllerAs = view.controllerAs; - dst[name] = result; - })); - }); - - // Wait for all the promises and then return the activation object - return $q.all(promises).then(function (values) { - return dst; - }); - } - - return $state; - } - - function shouldTriggerReload(to, from, locals, options) { - if ( to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false)) ) { - return true; - } - } -} - -angular.module('ui.router.state') - .value('$stateParams', {}) - .provider('$state', $StateProvider); - - -$ViewProvider.$inject = []; -function $ViewProvider() { - - this.$get = $get; - /** - * @ngdoc object - * @name ui.router.state.$view - * - * @requires ui.router.util.$templateFactory - * @requires $rootScope - * - * @description - * - */ - $get.$inject = ['$rootScope', '$templateFactory']; - function $get( $rootScope, $templateFactory) { - return { - // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) - /** - * @ngdoc function - * @name ui.router.state.$view#load - * @methodOf ui.router.state.$view - * - * @description - * - * @param {string} name name - * @param {object} options option object. - */ - load: function load(name, options) { - var result, defaults = { - template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} - }; - options = extend(defaults, options); - - if (options.view) { - result = $templateFactory.fromConfig(options.view, options.params, options.locals); - } - if (result && options.notify) { - /** - * @ngdoc event - * @name ui.router.state.$state#$viewContentLoading - * @eventOf ui.router.state.$view - * @eventType broadcast on root scope - * @description - * - * Fired once the view **begins loading**, *before* the DOM is rendered. - * - * @param {Object} event Event object. - * @param {Object} viewConfig The view config properties (template, controller, etc). - * - * @example - * - *
    -         * $scope.$on('$viewContentLoading',
    -         * function(event, viewConfig){
    -         *     // Access to all the view config properties.
    -         *     // and one special property 'targetView'
    -         *     // viewConfig.targetView
    -         * });
    -         * 
    - */ - $rootScope.$broadcast('$viewContentLoading', options); - } - return result; - } - }; - } -} - -angular.module('ui.router.state').provider('$view', $ViewProvider); - -/** - * @ngdoc object - * @name ui.router.state.$uiViewScrollProvider - * - * @description - * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. - */ -function $ViewScrollProvider() { - - var useAnchorScroll = false; - - /** - * @ngdoc function - * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll - * @methodOf ui.router.state.$uiViewScrollProvider - * - * @description - * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for - * scrolling based on the url anchor. - */ - this.useAnchorScroll = function () { - useAnchorScroll = true; - }; - - /** - * @ngdoc object - * @name ui.router.state.$uiViewScroll - * - * @requires $anchorScroll - * @requires $timeout - * - * @description - * When called with a jqLite element, it scrolls the element into view (after a - * `$timeout` so the DOM has time to refresh). - * - * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, - * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. - */ - this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { - if (useAnchorScroll) { - return $anchorScroll; - } - - return function ($element) { - $timeout(function () { - $element[0].scrollIntoView(); - }, 0, false); - }; - }]; -} - -angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); - -/** - * @ngdoc directive - * @name ui.router.state.directive:ui-view - * - * @requires ui.router.state.$state - * @requires $compile - * @requires $controller - * @requires $injector - * @requires ui.router.state.$uiViewScroll - * @requires $document - * - * @restrict ECA - * - * @description - * The ui-view directive tells $state where to place your templates. - * - * @param {string=} ui-view A view name. The name should be unique amongst the other views in the - * same state. You can have views of the same name that live in different states. - * - * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window - * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll - * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you - * scroll ui-view elements into view when they are populated during a state activation. - * - * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) - * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* - * - * @param {string=} onload Expression to evaluate whenever the view updates. - * - * @example - * A view can be unnamed or named. - *
    - * 
    - * 
    - * - * - *
    - *
    - * - * You can only have one unnamed view within any template (or root html). If you are only using a - * single view and it is unnamed then you can populate it like so: - *
    - * 
    - * $stateProvider.state("home", { - * template: "

    HELLO!

    " - * }) - *
    - * - * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} - * config property, by name, in this case an empty name: - *
    - * $stateProvider.state("home", {
    - *   views: {
    - *     "": {
    - *       template: "

    HELLO!

    " - * } - * } - * }) - *
    - * - * But typically you'll only use the views property if you name your view or have more than one view - * in the same template. There's not really a compelling reason to name a view if its the only one, - * but you could if you wanted, like so: - *
    - * 
    - *
    - *
    - * $stateProvider.state("home", {
    - *   views: {
    - *     "main": {
    - *       template: "

    HELLO!

    " - * } - * } - * }) - *
    - * - * Really though, you'll use views to set up multiple views: - *
    - * 
    - *
    - *
    - *
    - * - *
    - * $stateProvider.state("home", {
    - *   views: {
    - *     "": {
    - *       template: "

    HELLO!

    " - * }, - * "chart": { - * template: "" - * }, - * "data": { - * template: "" - * } - * } - * }) - *
    - * - * Examples for `autoscroll`: - * - *
    - * 
    - * 
    - *
    - * 
    - * 
    - * 
    - * 
    - * 
    - */ -$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll']; -function $ViewDirective( $state, $injector, $uiViewScroll) { - - function getService() { - return ($injector.has) ? function(service) { - return $injector.has(service) ? $injector.get(service) : null; - } : function(service) { - try { - return $injector.get(service); - } catch (e) { - return null; - } - }; - } - - var service = getService(), - $animator = service('$animator'), - $animate = service('$animate'); - - // Returns a set of DOM manipulation functions based on which Angular version - // it should use - function getRenderer(attrs, scope) { - var statics = function() { - return { - enter: function (element, target, cb) { target.after(element); cb(); }, - leave: function (element, cb) { element.remove(); cb(); } - }; - }; - - if ($animate) { - return { - enter: function(element, target, cb) { $animate.enter(element, null, target, cb); }, - leave: function(element, cb) { $animate.leave(element, cb); } - }; - } - - if ($animator) { - var animate = $animator && $animator(scope, attrs); - - return { - enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, - leave: function(element, cb) { animate.leave(element); cb(); } - }; - } - - return statics(); - } - - var directive = { - restrict: 'ECA', - terminal: true, - priority: 400, - transclude: 'element', - compile: function (tElement, tAttrs, $transclude) { - return function (scope, $element, attrs) { - var previousEl, currentEl, currentScope, latestLocals, - onloadExp = attrs.onload || '', - autoScrollExp = attrs.autoscroll, - renderer = getRenderer(attrs, scope); - - scope.$on('$stateChangeSuccess', function() { - updateView(false); - }); - scope.$on('$viewContentLoading', function() { - updateView(false); - }); - - updateView(true); - - function cleanupLastView() { - if (previousEl) { - previousEl.remove(); - previousEl = null; - } - - if (currentScope) { - currentScope.$destroy(); - currentScope = null; - } - - if (currentEl) { - renderer.leave(currentEl, function() { - previousEl = null; - }); - - previousEl = currentEl; - currentEl = null; - } - } - - function updateView(firstTime) { - var newScope = scope.$new(), - name = currentEl && currentEl.data('$uiViewName'), - previousLocals = name && $state.$current && $state.$current.locals[name]; - - if (!firstTime && previousLocals === latestLocals) return; // nothing to do - - var clone = $transclude(newScope, function(clone) { - renderer.enter(clone, $element, function onUiViewEnter() { - if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { - $uiViewScroll(clone); - } - }); - cleanupLastView(); - }); - - latestLocals = $state.$current.locals[clone.data('$uiViewName')]; - - currentEl = clone; - currentScope = newScope; - /** - * @ngdoc event - * @name ui.router.state.directive:ui-view#$viewContentLoaded - * @eventOf ui.router.state.directive:ui-view - * @eventType emits on ui-view directive scope - * @description * - * Fired once the view is **loaded**, *after* the DOM is rendered. - * - * @param {Object} event Event object. - */ - currentScope.$emit('$viewContentLoaded'); - currentScope.$eval(onloadExp); - } - }; - } - }; - - return directive; -} - -$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state']; -function $ViewDirectiveFill ($compile, $controller, $state) { - return { - restrict: 'ECA', - priority: -400, - compile: function (tElement) { - var initial = tElement.html(); - return function (scope, $element, attrs) { - var name = attrs.uiView || attrs.name || '', - inherited = $element.inheritedData('$uiView'); - - if (name.indexOf('@') < 0) { - name = name + '@' + (inherited ? inherited.state.name : ''); - } - - $element.data('$uiViewName', name); - - var current = $state.$current, - locals = current && current.locals[name]; - - if (! locals) { - return; - } - - $element.data('$uiView', { name: name, state: locals.$$state }); - $element.html(locals.$template ? locals.$template : initial); - - var link = $compile($element.contents()); - - if (locals.$$controller) { - locals.$scope = scope; - var controller = $controller(locals.$$controller, locals); - if (locals.$$controllerAs) { - scope[locals.$$controllerAs] = controller; - } - $element.data('$ngControllerController', controller); - $element.children().data('$ngControllerController', controller); - } - - link(scope); - }; - } - }; -} - -angular.module('ui.router.state').directive('uiView', $ViewDirective); -angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); - -function parseStateRef(ref) { - var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); - if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); - return { state: parsed[1], paramExpr: parsed[3] || null }; -} - -function stateContext(el) { - var stateData = el.parent().inheritedData('$uiView'); - - if (stateData && stateData.state && stateData.state.name) { - return stateData.state; - } -} - -/** - * @ngdoc directive - * @name ui.router.state.directive:ui-sref - * - * @requires ui.router.state.$state - * @requires $timeout - * - * @restrict A - * - * @description - * A directive that binds a link (`` tag) to a state. If the state has an associated - * URL, the directive will automatically generate & update the `href` attribute via - * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking - * the link will trigger a state transition with optional parameters. - * - * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be - * handled natively by the browser. - * - * You can also use relative state paths within ui-sref, just like the relative - * paths passed to `$state.go()`. You just need to be aware that the path is relative - * to the state that the link lives in, in other words the state that loaded the - * template containing the link. - * - * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} - * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, - * and `reload`. - * - * @example - * Here's an example of how you'd use ui-sref and how it would compile. If you have the - * following template: - *
    - * Home | About
    - * 
    - * 
    - * 
    - * - * Then the compiled html would be (assuming Html5Mode is off): - *
    - * Home | About
    - * 
    - * 
      - *
    • - * Joe - *
    • - *
    • - * Alice - *
    • - *
    • - * Bob - *
    • - *
    - * - * Home - *
    - * - * @param {string} ui-sref 'stateName' can be any valid absolute or relative state - * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} - */ -$StateRefDirective.$inject = ['$state', '$timeout']; -function $StateRefDirective($state, $timeout) { - var allowedOptions = ['location', 'inherit', 'reload']; - - return { - restrict: 'A', - require: '?^uiSrefActive', - link: function(scope, element, attrs, uiSrefActive) { - var ref = parseStateRef(attrs.uiSref); - var params = null, url = null, base = stateContext(element) || $state.$current; - var isForm = element[0].nodeName === "FORM"; - var attr = isForm ? "action" : "href", nav = true; - - var options = { - relative: base - }; - var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; - angular.forEach(allowedOptions, function(option) { - if (option in optionsOverride) { - options[option] = optionsOverride[option]; - } - }); - - var update = function(newVal) { - if (newVal) params = newVal; - if (!nav) return; - - var newHref = $state.href(ref.state, params, options); - - if (uiSrefActive) { - uiSrefActive.$$setStateInfo(ref.state, params); - } - if (!newHref) { - nav = false; - return false; - } - element[0][attr] = newHref; - }; - - if (ref.paramExpr) { - scope.$watch(ref.paramExpr, function(newVal, oldVal) { - if (newVal !== params) update(newVal); - }, true); - params = scope.$eval(ref.paramExpr); - } - update(); - - if (isForm) return; - - element.bind("click", function(e) { - var button = e.which || e.button; - if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) { - // HACK: This is to allow ng-clicks to be processed before the transition is initiated: - $timeout(function() { - $state.go(ref.state, params, options); - }); - e.preventDefault(); - } - }); - } - }; D -} - -/** - * @ngdoc directive - * @name ui.router.state.directive:ui-sref-active - * - * @requires ui.router.state.$state - * @requires ui.router.state.$stateParams - * @requires $interpolate - * - * @restrict A - * - * @description - * A directive working alongside ui-sref to add classes to an element when the - * related ui-sref directive's state is active, and removing them when it is inactive. - * The primary use-case is to simplify the special appearance of navigation menus - * relying on `ui-sref`, by having the "active" state's menu button appear different, - * distinguishing it from the inactive menu items. - * - * @example - * Given the following template: - *
    - * 
    - * 
    - * - * When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins", - * the resulting HTML will appear as (note the 'active' class): - *
    - * 
    - * 
    - * - * The class name is interpolated **once** during the directives link time (any further changes to the - * interpolated value are ignored). - * - * Multiple classes may be specified in a space-separated format: - *
    - * 
      - *
    • - * link - *
    • - *
    - *
    - */ -$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; -function $StateActiveDirective($state, $stateParams, $interpolate) { - return { - restrict: "A", - controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { - var state, params, activeClass; - - // There probably isn't much point in $observing this - activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope); - - // Allow uiSref to communicate with uiSrefActive - this.$$setStateInfo = function(newState, newParams) { - state = $state.get(newState, stateContext($element)); - params = newParams; - update(); - }; - - $scope.$on('$stateChangeSuccess', update); - - // Update route state - function update() { - if ($state.$current.self === state && matchesParams()) { - $element.addClass(activeClass); - } else { - $element.removeClass(activeClass); - } - } - - function matchesParams() { - return !params || equalForKeys(params, $stateParams); - } - }] - }; -} - -angular.module('ui.router.state') - .directive('uiSref', $StateRefDirective) - .directive('uiSrefActive', $StateActiveDirective); - -/** - * @ngdoc filter - * @name ui.router.state.filter:isState - * - * @requires ui.router.state.$state - * - * @description - * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. - */ -$IsStateFilter.$inject = ['$state']; -function $IsStateFilter($state) { - return function(state) { - return $state.is(state); - }; -} - -/** - * @ngdoc filter - * @name ui.router.state.filter:includedByState - * - * @requires ui.router.state.$state - * - * @description - * Translates to {@link ui.router.state.$state#methods_includes $state.includes('fullOrPartialStateName')}. - */ -$IncludedByStateFilter.$inject = ['$state']; -function $IncludedByStateFilter($state) { - return function(state) { - return $state.includes(state); - }; -} - -angular.module('ui.router.state') - .filter('isState', $IsStateFilter) - .filter('includedByState', $IncludedByStateFilter); - -/* - * @ngdoc object - * @name ui.router.compat.$routeProvider - * - * @requires ui.router.state.$stateProvider - * @requires ui.router.router.$urlRouterProvider - * - * @description - * `$routeProvider` of the `ui.router.compat` module overwrites the existing - * `routeProvider` from the core. This is done to provide compatibility between - * the UI Router and the core router. - * - * It also provides a `when()` method to register routes that map to certain urls. - * Behind the scenes it actually delegates either to - * {@link ui.router.router.$urlRouterProvider $urlRouterProvider} or to the - * {@link ui.router.state.$stateProvider $stateProvider} to postprocess the given - * router definition object. - */ -$RouteProvider.$inject = ['$stateProvider', '$urlRouterProvider']; -function $RouteProvider( $stateProvider, $urlRouterProvider) { - - var routes = []; - - onEnterRoute.$inject = ['$$state']; - function onEnterRoute( $$state) { - /*jshint validthis: true */ - this.locals = $$state.locals.globals; - this.params = this.locals.$stateParams; - } - - function onExitRoute() { - /*jshint validthis: true */ - this.locals = null; - this.params = null; - } - - this.when = when; - /* - * @ngdoc function - * @name ui.router.compat.$routeProvider#when - * @methodOf ui.router.compat.$routeProvider - * - * @description - * Registers a route with a given route definition object. The route definition - * object has the same interface the angular core route definition object has. - * - * @example - *
    -   * var app = angular.module('app', ['ui.router.compat']);
    -   *
    -   * app.config(function ($routeProvider) {
    -   *   $routeProvider.when('home', {
    -   *     controller: function () { ... },
    -   *     templateUrl: 'path/to/template'
    -   *   });
    -   * });
    -   * 
    - * - * @param {string} url URL as string - * @param {object} route Route definition object - * - * @return {object} $routeProvider - $routeProvider instance - */ - function when(url, route) { - /*jshint validthis: true */ - if (route.redirectTo != null) { - // Redirect, configure directly on $urlRouterProvider - var redirect = route.redirectTo, handler; - if (isString(redirect)) { - handler = redirect; // leave $urlRouterProvider to handle - } else if (isFunction(redirect)) { - // Adapt to $urlRouterProvider API - handler = function (params, $location) { - return redirect(params, $location.path(), $location.search()); - }; - } else { - throw new Error("Invalid 'redirectTo' in when()"); - } - $urlRouterProvider.when(url, handler); - } else { - // Regular route, configure as state - $stateProvider.state(inherit(route, { - parent: null, - name: 'route:' + encodeURIComponent(url), - url: url, - onEnter: onEnterRoute, - onExit: onExitRoute - })); - } - routes.push(route); - return this; - } - - /* - * @ngdoc object - * @name ui.router.compat.$route - * - * @requires ui.router.state.$state - * @requires $rootScope - * @requires $routeParams - * - * @property {object} routes - Array of registered routes. - * @property {object} params - Current route params as object. - * @property {string} current - Name of the current route. - * - * @description - * The `$route` service provides interfaces to access defined routes. It also let's - * you access route params through `$routeParams` service, so you have fully - * control over all the stuff you would actually get from angular's core `$route` - * service. - */ - this.$get = $get; - $get.$inject = ['$state', '$rootScope', '$routeParams']; - function $get( $state, $rootScope, $routeParams) { - - var $route = { - routes: routes, - params: $routeParams, - current: undefined - }; - - function stateAsRoute(state) { - return (state.name !== '') ? state : undefined; - } - - $rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) { - $rootScope.$broadcast('$routeChangeStart', stateAsRoute(to), stateAsRoute(from)); - }); - - $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) { - $route.current = stateAsRoute(to); - $rootScope.$broadcast('$routeChangeSuccess', stateAsRoute(to), stateAsRoute(from)); - copy(toParams, $route.params); - }); - - $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) { - $rootScope.$broadcast('$routeChangeError', stateAsRoute(to), stateAsRoute(from), error); - }); - - return $route; - } -} - -angular.module('ui.router.compat') - .provider('$route', $RouteProvider) - .directive('ngView', $ViewDirective); -})(window, window.angular); \ No newline at end of file diff --git a/src/main/webapp/new/lib/angular-ui-router.min.js b/src/main/webapp/new/lib/angular-ui-router.min.js deleted file mode 100644 index f065ecc9..00000000 --- a/src/main/webapp/new/lib/angular-ui-router.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * State-based routing for AngularJS - * @version v0.2.10 - * @link http://angular-ui.github.com/ - * @license MIT License, http://www.opensource.org/licenses/MIT - */ -"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return I(new(I(function(){},{prototype:a})),b)}function e(a){return H(arguments,function(b){b!==a&&H(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function h(a,b,c,d){var e,h=f(c,d),i={},j=[];for(var k in h)if(h[k].params&&h[k].params.length){e=h[k].params;for(var l in e)g(j,e[l])>=0||(j.push(e[l]),i[e[l]]=a[e[l]])}return I({},i,b)}function i(a,b){var c={};return H(a,function(a){var d=b[a];c[a]=null!=d?String(d):null}),c}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(o[c]=d,E(a))m.push(c,[function(){return b.get(a)}],h);else{var e=b.annotate(a);H(e,function(a){a!==c&&g.hasOwnProperty(a)&&k(g[a],a)}),m.push(c,a,e)}n.pop(),o[c]=f}}function l(a){return F(a)&&a.then&&a.$$promises}if(!F(g))throw new Error("'invocables' must be an object");var m=[],n=[],o={};return H(g,k),g=n=o=null,function(d,f,g){function h(){--s||(t||e(r,f.$$values),p.$$values=r,p.$$promises=!0,o.resolve(r))}function k(a){p.$$failure=a,o.reject(a)}function n(c,e,f){function i(a){l.reject(a),k(a)}function j(){if(!C(p.$$failure))try{l.resolve(b.invoke(e,g,r)),l.promise.then(function(a){r[c]=a,h()},i)}catch(a){i(a)}}var l=a.defer(),m=0;H(f,function(a){q.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,q[a].then(function(b){r[a]=b,--m||j()},i))}),m||j(),q[c]=l.promise}if(l(d)&&g===c&&(g=f,f=d,d=null),d){if(!F(d))throw new Error("'locals' must be an object")}else d=i;if(f){if(!l(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=j;var o=a.defer(),p=o.promise,q=p.$$promises={},r=I({},d),s=1+m.length/3,t=!1;if(C(f.$$failure))return k(f.$$failure),p;f.$$values?(t=e(r,f.$$values),h()):(I(q,f.$$promises),f.then(h,k));for(var u=0,v=m.length;v>u;u+=3)d.hasOwnProperty(m[u])?h():n(m[u],m[u+1],m[u+2]);return p}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function m(a,b,c){this.fromConfig=function(a,b,c){return C(a.template)?this.fromString(a.template,b):C(a.templateUrl)?this.fromUrl(a.templateUrl,b):C(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return D(a)?a(b):a},this.fromUrl=function(c,d){return D(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function n(a){function b(b){if(!/^\w+(-+\w+)*$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(f[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");f[b]=!0,j.push(b)}function c(a){return a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&")}var d,e=/([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,f={},g="^",h=0,i=this.segments=[],j=this.params=[];this.source=a;for(var k,l,m;(d=e.exec(a))&&(k=d[2]||d[3],l=d[4]||("*"==d[1]?".*":"[^/]*"),m=a.substring(h,d.index),!(m.indexOf("?")>=0));)g+=c(m)+"("+l+")",b(k),i.push(m),h=e.lastIndex;m=a.substring(h);var n=m.indexOf("?");if(n>=0){var o=this.sourceSearch=m.substring(n);m=m.substring(0,n),this.sourcePath=a.substring(0,h+n),H(o.substring(1).split(/[&?]/),b)}else this.sourcePath=a,this.sourceSearch="";g+=c(m)+"$",i.push(m),this.regexp=new RegExp(g),this.prefix=i[0]}function o(){this.compile=function(a){return new n(a)},this.isMatcher=function(a){return F(a)&&D(a.exec)&&D(a.format)&&D(a.concat)},this.$get=function(){return this}}function p(a){function b(a){var b=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(a.source);return null!=b?b[1].replace(/\\(.)/g,"$1"):""}function c(a,b){return a.replace(/\$(\$|\d{1,2})/,function(a,c){return b["$"===c?0:Number(c)]})}function d(a,b,c){if(!c)return!1;var d=a.invoke(b,b,{$match:c});return C(d)?d:!0}var e=[],f=null;this.rule=function(a){if(!D(a))throw new Error("'rule' must be a function");return e.push(a),this},this.otherwise=function(a){if(E(a)){var b=a;a=function(){return b}}else if(!D(a))throw new Error("'rule' must be a function");return f=a,this},this.when=function(e,f){var g,h=E(f);if(E(e)&&(e=a.compile(e)),!h&&!D(f)&&!G(f))throw new Error("invalid 'handler' in when()");var i={matcher:function(b,c){return h&&(g=a.compile(c),c=["$match",function(a){return g.format(a)}]),I(function(a,e){return d(a,c,b.exec(e.path(),e.search()))},{prefix:E(b.prefix)?b.prefix:""})},regex:function(a,e){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(g=e,e=["$match",function(a){return c(g,a)}]),I(function(b,c){return d(b,e,a.exec(c.path()))},{prefix:b(a)})}},j={matcher:a.isMatcher(e),regex:e instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](e,f));throw new Error("invalid 'what' in when()")},this.$get=["$location","$rootScope","$injector",function(a,b,c){function d(b){function d(b){var d=b(c,a);return d?(E(d)&&a.replace().url(d),!0):!1}if(!b||!b.defaultPrevented){var g,h=e.length;for(g=0;h>g;g++)if(d(e[g]))return;f&&d(f)}}return b.$on("$locationChangeSuccess",d),{sync:function(){d()}}}]}function q(a,e,f){function g(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function l(a,b){var d=E(a),e=d?a:a.name,f=g(e);if(f){if(!b)throw new Error("No reference point given for path '"+e+"'");for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=w[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function m(a,b){x[a]||(x[a]=[]),x[a].push(b)}function n(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!E(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(w.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):E(b.parent)?b.parent:"";if(e&&!w[e])return m(e,b.self);for(var f in z)D(z[f])&&(b[f]=z[f](b,z.$delegates[f]));if(w[c]=b,!b[y]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){v.$current.navigable==b&&j(a,c)||v.transitionTo(b,a,{location:!1})}]),x[c])for(var g=0;g-1}function p(a){var b=a.split("."),c=v.$current.name.split(".");if("**"===b[0]&&(c=c.slice(c.indexOf(b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(c.indexOf(b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length)return!1;for(var d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return c.join("")===b.join("")}function q(a,b){return E(a)&&!C(b)?z[a]:D(b)&&E(a)?(z[a]&&!z.$delegates[a]&&(z.$delegates[a]=z[a]),z[a]=b,this):this}function r(a,b){return F(a)?b=a:b.name=a,n(b),this}function s(a,e,g,m,n,q,r,s,x){function z(){r.url()!==M&&(r.url(M),r.replace())}function A(a,c,d,f,h){var i=d?c:k(a.params,c),j={$stateParams:i};h.resolve=n.resolve(a.resolve,j,h.resolve,a);var l=[h.resolve.then(function(a){h.globals=a})];return f&&l.push(f),H(a.views,function(c,d){var e=c.resolve&&c.resolve!==a.resolve?c.resolve:{};e.$template=[function(){return g.load(d,{view:c,locals:j,params:i,notify:!1})||""}],l.push(n.resolve(e,j,h.resolve,a).then(function(f){if(D(c.controllerProvider)||G(c.controllerProvider)){var g=b.extend({},e,j);f.$$controller=m.invoke(c.controllerProvider,null,g)}else f.$$controller=c.controller;f.$$state=a,f.$$controllerAs=c.controllerAs,h[d]=f}))}),e.all(l).then(function(){return h})}var B=e.reject(new Error("transition superseded")),F=e.reject(new Error("transition prevented")),K=e.reject(new Error("transition aborted")),L=e.reject(new Error("transition failed")),M=r.url(),N=x.baseHref();return u.locals={resolve:null,globals:{$stateParams:{}}},v={params:{},current:u.self,$current:u,transition:null},v.reload=function(){v.transitionTo(v.current,q,{reload:!0,inherit:!1,notify:!1})},v.go=function(a,b,c){return this.transitionTo(a,b,I({inherit:!0,relative:v.$current},c))},v.transitionTo=function(b,c,f){c=c||{},f=I({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,k=v.$current,n=v.params,o=k.path,p=l(b,f.relative);if(!C(p)){var s={to:b,toParams:c,options:f};if(g=a.$broadcast("$stateNotFound",s,k.self,n),g.defaultPrevented)return z(),K;if(g.retry){if(f.$retry)return z(),L;var w=v.transition=e.when(g.retry);return w.then(function(){return w!==v.transition?B:(s.options.$retry=!0,v.transitionTo(s.to,s.toParams,s.options))},function(){return K}),z(),w}if(b=s.to,c=s.toParams,f=s.options,p=l(b,f.relative),!C(p)){if(f.relative)throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'");throw new Error("No such state '"+b+"'")}}if(p[y])throw new Error("Cannot transition to abstract state '"+b+"'");f.inherit&&(c=h(q,c||{},v.$current,p)),b=p;var x,D,E=b.path,G=u.locals,H=[];for(x=0,D=E[x];D&&D===o[x]&&j(c,n,D.ownParams)&&!f.reload;x++,D=E[x])G=H[x]=D.locals;if(t(b,k,G,f))return b.self.reloadOnSearch!==!1&&z(),v.transition=null,e.when(v.current);if(c=i(b.params,c||{}),f.notify&&(g=a.$broadcast("$stateChangeStart",b.self,c,k.self,n),g.defaultPrevented))return z(),F;for(var N=e.when(G),O=x;O=x;d--)g=o[d],g.self.onExit&&m.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=x;d1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target")||(c(function(){a.go(i.state,j,o)}),b.preventDefault())})}}}function y(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(d,e,f){function g(){a.$current.self===i&&h()?e.addClass(l):e.removeClass(l)}function h(){return!k||j(k,b)}var i,k,l;l=c(f.uiSrefActive||"",!1)(d),this.$$setStateInfo=function(b,c){i=a.get(b,w(e)),k=c,g()},d.$on("$stateChangeSuccess",g)}]}}function z(a){return function(b){return a.is(b)}}function A(a){return function(b){return a.includes(b)}}function B(a,b){function e(a){this.locals=a.locals.globals,this.params=this.locals.$stateParams}function f(){this.locals=null,this.params=null}function g(c,g){if(null!=g.redirectTo){var h,j=g.redirectTo;if(E(j))h=j;else{if(!D(j))throw new Error("Invalid 'redirectTo' in when()");h=function(a,b){return j(a,b.path(),b.search())}}b.when(c,h)}else a.state(d(g,{parent:null,name:"route:"+encodeURIComponent(c),url:c,onEnter:e,onExit:f}));return i.push(g),this}function h(a,b,d){function e(a){return""!==a.name?a:c}var f={routes:i,params:d,current:c};return b.$on("$stateChangeStart",function(a,c,d,f){b.$broadcast("$routeChangeStart",e(c),e(f))}),b.$on("$stateChangeSuccess",function(a,c,d,g){f.current=e(c),b.$broadcast("$routeChangeSuccess",e(c),e(g)),J(d,f.params)}),b.$on("$stateChangeError",function(a,c,d,f,g,h){b.$broadcast("$routeChangeError",e(c),e(f),h)}),f}var i=[];e.$inject=["$$state"],this.when=g,this.$get=h,h.$inject=["$state","$rootScope","$routeParams"]}var C=b.isDefined,D=b.isFunction,E=b.isString,F=b.isObject,G=b.isArray,H=b.forEach,I=b.extend,J=b.copy;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),l.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",l),m.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",m),n.prototype.concat=function(a){return new n(this.sourcePath+a+this.sourceSearch)},n.prototype.toString=function(){return this.source},n.prototype.exec=function(a,b){var c=this.regexp.exec(a);if(!c)return null;var d,e=this.params,f=e.length,g=this.segments.length-1,h={};if(g!==c.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");for(d=0;g>d;d++)h[e[d]]=c[d+1];for(;f>d;d++)h[e[d]]=b[e[d]];return h},n.prototype.parameters=function(){return this.params},n.prototype.format=function(a){var b=this.segments,c=this.params;if(!a)return b.join("");var d,e,f,g=b.length-1,h=c.length,i=b[0];for(d=0;g>d;d++)f=a[c[d]],null!=f&&(i+=encodeURIComponent(f)),i+=b[d+1];for(;h>d;d++)f=a[c[d]],null!=f&&(i+=(e?"&":"?")+c[d]+"="+encodeURIComponent(f),e=!0);return i},b.module("ui.router.util").provider("$urlMatcherFactory",o),p.$inject=["$urlMatcherFactoryProvider"],b.module("ui.router.router").provider("$urlRouter",p),q.$inject=["$urlRouterProvider","$urlMatcherFactoryProvider","$locationProvider"],b.module("ui.router.state").value("$stateParams",{}).provider("$state",q),r.$inject=[],b.module("ui.router.state").provider("$view",r),b.module("ui.router.state").provider("$uiViewScroll",s),t.$inject=["$state","$injector","$uiViewScroll"],u.$inject=["$compile","$controller","$state"],b.module("ui.router.state").directive("uiView",t),b.module("ui.router.state").directive("uiView",u),x.$inject=["$state","$timeout"],y.$inject=["$state","$stateParams","$interpolate"],b.module("ui.router.state").directive("uiSref",x).directive("uiSrefActive",y),z.$inject=["$state"],A.$inject=["$state"],b.module("ui.router.state").filter("isState",z).filter("includedByState",A),B.$inject=["$stateProvider","$urlRouterProvider"],b.module("ui.router.compat").provider("$route",B).directive("ngView",t)}(window,window.angular); \ No newline at end of file diff --git a/src/main/webapp/new/lib/angular/angular-animate.js b/src/main/webapp/new/lib/angular/angular-animate.js deleted file mode 100644 index 60f8beb8..00000000 --- a/src/main/webapp/new/lib/angular/angular-animate.js +++ /dev/null @@ -1,1729 +0,0 @@ -/** - * @license AngularJS v1.3.0-beta.17 - * (c) 2010-2014 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -/* jshint maxlen: false */ - -/** - * @ngdoc module - * @name ngAnimate - * @description - * - * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. - * - *
    - * - * # Usage - * - * To see animations in action, all that is required is to define the appropriate CSS classes - * or to register a JavaScript animation via the myModule.animation() function. The directives that support animation automatically are: - * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation - * by using the `$animate` service. - * - * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: - * - * | Directive | Supported Animations | - * |-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| - * | {@link ng.directive:ngRepeat#usage_animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | - * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove (the CSS class(es) present) | - * | {@link ng.directive:ngShow#usage_animations ngShow} & {@link ng.directive:ngHide#usage_animations ngHide} | add and remove (the ng-hide class value) | - * | {@link ng.directive:form#usage_animations form} & {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | - * | {@link ngMessages.directive:ngMessage#usage_animations ngMessages} | add and remove (ng-active & ng-inactive) | - * | {@link ngMessages.directive:ngMessage#usage_animations ngMessage} | enter and leave | - * - * You can find out more information about animations upon visiting each directive page. - * - * Below is an example of how to apply animations to a directive that supports animation hooks: - * - * ```html - * - * - * - * - * ``` - * - * Keep in mind that, by default, if an animation is running, any child elements cannot be animated - * until the parent element's animation has completed. This blocking feature can be overridden by - * placing the `ng-animate-children` attribute on a parent container tag. - * - * ```html - *
    - *
    - *
    - * ... - *
    - *
    - *
    - * ``` - * - * When the `on` expression value changes and an animation is triggered then each of the elements within - * will all animate without the block being applied to child elements. - * - *

    CSS-defined Animations

    - * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes - * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported - * and can be used to play along with this naming structure. - * - * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: - * - * ```html - * - * - *
    - *
    - *
    - * ``` - * - * The following code below demonstrates how to perform animations using **CSS animations** with Angular: - * - * ```html - * - * - *
    - *
    - *
    - * ``` - * - * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. - * - * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add - * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically - * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be - * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end - * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element - * has no CSS transition/animation classes applied to it. - * - * ### Structural transition animations - * - * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition - * value to force the browser into rendering the styles defined in the setup (.ng-enter, .ng-leave - * or .ng-move) class. This means that any active transition animations operating on the element - * will be cut off to make way for the enter, leave or move animation. - * - * ### Class-based transition animations - * - * Class-based transitions refer to transition animations that are triggered when a CSS class is - * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`, - * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`). - * They are different when compared to structural animations since they **do not cancel existing - * animations** nor do they **block successive transitions** from rendering on the same element. - * This distinction allows for **multiple class-based transitions** to be performed on the same element. - * - * In addition to ngAnimate supporting the default (natural) functionality of class-based transition - * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the - * developer in further styling the element throughout the transition animation. Earlier versions - * of ngAnimate may have caused natural CSS transitions to break and not render properly due to - * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class - * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of - * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS - * class transitions are compatible with ngAnimate. - * - * There is, however, one special case when dealing with class-based transitions in ngAnimate. - * When rendering class-based transitions that make use of the setup and active CSS classes - * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define - * the transition value **on the active CSS class** and not the setup class. - * - * ```css - * .fade-add { - * /* remember to place a 0s transition here - * to ensure that the styles are applied instantly - * even if the element already has a transition style */ - * transition:0s linear all; - * - * /* starting CSS styles */ - * opacity:1; - * } - * .fade-add.fade-add-active { - * /* this will be the length of the animation */ - * transition:1s linear all; - * opacity:0; - * } - * ``` - * - * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it - * has a duration of zero. This may not be required, however, incase the browser is unable to render - * the styling present in this CSS class instantly then it could be that the browser is attempting - * to perform an unnecessary transition. - * - * This workaround, however, does not apply to standard class-based transitions that are rendered - * when a CSS class containing a transition is applied to an element: - * - * ```css - * .fade { - * /* this works as expected */ - * transition:1s linear all; - * opacity:0; - * } - * ``` - * - * Please keep this in mind when coding the CSS markup that will be used within class-based transitions. - * Also, try not to mix the two class-based animation flavors together since the CSS code may become - * overly complex. - * - * ### CSS Staggering Animations - * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a - * curtain-like effect. The ngAnimate module, as of 1.2.0, supports staggering animations and the stagger effect can be - * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for - * the animation. The style property expected within the stagger class can either be a **transition-delay** or an - * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). - * - * ```css - * .my-animation.ng-enter { - * /* standard transition code */ - * -webkit-transition: 1s linear all; - * transition: 1s linear all; - * opacity:0; - * } - * .my-animation.ng-enter-stagger { - * /* this will have a 100ms delay between each successive leave animation */ - * -webkit-transition-delay: 0.1s; - * transition-delay: 0.1s; - * - * /* in case the stagger doesn't work then these two values - * must be set to 0 to avoid an accidental CSS inheritance */ - * -webkit-transition-duration: 0s; - * transition-duration: 0s; - * } - * .my-animation.ng-enter.ng-enter-active { - * /* standard transition styles */ - * opacity:1; - * } - * ``` - * - * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations - * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this - * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation - * will also be reset if more than 10ms has passed after the last animation has been fired. - * - * The following code will issue the **ng-leave-stagger** event on the element provided: - * - * ```js - * var kids = parent.children(); - * - * $animate.leave(kids[0]); //stagger index=0 - * $animate.leave(kids[1]); //stagger index=1 - * $animate.leave(kids[2]); //stagger index=2 - * $animate.leave(kids[3]); //stagger index=3 - * $animate.leave(kids[4]); //stagger index=4 - * - * $timeout(function() { - * //stagger has reset itself - * $animate.leave(kids[5]); //stagger index=0 - * $animate.leave(kids[6]); //stagger index=1 - * }, 100, false); - * ``` - * - * Stagger animations are currently only supported within CSS-defined animations. - * - *

    JavaScript-defined Animations

    - * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not - * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. - * - * ```js - * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. - * var ngModule = angular.module('YourApp', ['ngAnimate']); - * ngModule.animation('.my-crazy-animation', function() { - * return { - * enter: function(element, done) { - * //run the animation here and call done when the animation is complete - * return function(cancelled) { - * //this (optional) function will be called when the animation - * //completes or when the animation is cancelled (the cancelled - * //flag will be set to true if cancelled). - * }; - * }, - * leave: function(element, done) { }, - * move: function(element, done) { }, - * - * //animation that can be triggered before the class is added - * beforeAddClass: function(element, className, done) { }, - * - * //animation that can be triggered after the class is added - * addClass: function(element, className, done) { }, - * - * //animation that can be triggered before the class is removed - * beforeRemoveClass: function(element, className, done) { }, - * - * //animation that can be triggered after the class is removed - * removeClass: function(element, className, done) { } - * }; - * }); - * ``` - * - * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run - * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits - * the element's CSS class attribute value and then run the matching animation event function (if found). - * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will - * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported). - * - * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. - * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, - * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation - * or transition code that is defined via a stylesheet). - * - */ - -angular.module('ngAnimate', ['ng']) - - /** - * @ngdoc provider - * @name $animateProvider - * @description - * - * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module. - * When an animation is triggered, the $animate service will query the $animate service to find any animations that match - * the provided name value. - * - * Requires the {@link ngAnimate `ngAnimate`} module to be installed. - * - * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. - * - */ - .directive('ngAnimateChildren', function() { - var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; - return function(scope, element, attrs) { - var val = attrs.ngAnimateChildren; - if(angular.isString(val) && val.length === 0) { //empty attribute - element.data(NG_ANIMATE_CHILDREN, true); - } else { - scope.$watch(val, function(value) { - element.data(NG_ANIMATE_CHILDREN, !!value); - }); - } - }; - }) - - //this private service is only used within CSS-enabled animations - //IE8 + IE9 do not support rAF natively, but that is fine since they - //also don't support transitions and keyframes which means that the code - //below will never be used by the two browsers. - .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) { - var bod = $document[0].body; - return function(fn) { - //the returned function acts as the cancellation function - return $$rAF(function() { - //the line below will force the browser to perform a repaint - //so that all the animated elements within the animation frame - //will be properly updated and drawn on screen. This is - //required to perform multi-class CSS based animations with - //Firefox. DO NOT REMOVE THIS LINE. - var a = bod.offsetWidth + 1; - fn(); - }); - }; - }]) - - .config(['$provide', '$animateProvider', function($provide, $animateProvider) { - var noop = angular.noop; - var forEach = angular.forEach; - var selectors = $animateProvider.$$selectors; - - var ELEMENT_NODE = 1; - var NG_ANIMATE_STATE = '$$ngAnimateState'; - var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; - var NG_ANIMATE_CLASS_NAME = 'ng-animate'; - var rootAnimateState = {running: true}; - - function extractElementNode(element) { - for(var i = 0; i < element.length; i++) { - var elm = element[i]; - if(elm.nodeType == ELEMENT_NODE) { - return elm; - } - } - } - - function prepareElement(element) { - return element && angular.element(element); - } - - function stripCommentsFromElement(element) { - return angular.element(extractElementNode(element)); - } - - function isMatchingElement(elm1, elm2) { - return extractElementNode(elm1) == extractElementNode(elm2); - } - - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', - function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) { - - var globalAnimationCounter = 0; - $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); - - // disable animations during bootstrap, but once we bootstrapped, wait again - // for another digest until enabling animations. The reason why we digest twice - // is because all structural animations (enter, leave and move) all perform a - // post digest operation before animating. If we only wait for a single digest - // to pass then the structural animation would render its animation on page load. - // (which is what we're trying to avoid when the application first boots up.) - $rootScope.$$postDigest(function() { - $rootScope.$$postDigest(function() { - rootAnimateState.running = false; - }); - }); - - var classNameFilter = $animateProvider.classNameFilter(); - var isAnimatableClassName = !classNameFilter - ? function() { return true; } - : function(className) { - return classNameFilter.test(className); - }; - - function blockElementAnimations(element) { - var data = element.data(NG_ANIMATE_STATE) || {}; - data.running = true; - element.data(NG_ANIMATE_STATE, data); - } - - function runAnimationPostDigest(fn) { - var cancelFn; - $rootScope.$$postDigest(function() { - cancelFn = fn(); - }); - return function() { - cancelFn && cancelFn(); - }; - } - - function lookup(name) { - if (name) { - var matches = [], - flagMap = {}, - classes = name.substr(1).split('.'); - - //the empty string value is the default animation - //operation which performs CSS transition and keyframe - //animations sniffing. This is always included for each - //element animation procedure if the browser supports - //transitions and/or keyframe animations. The default - //animation is added to the top of the list to prevent - //any previous animations from affecting the element styling - //prior to the element being animated. - if ($sniffer.transitions || $sniffer.animations) { - matches.push($injector.get(selectors[''])); - } - - for(var i=0; i < classes.length; i++) { - var klass = classes[i], - selectorFactoryName = selectors[klass]; - if(selectorFactoryName && !flagMap[klass]) { - matches.push($injector.get(selectorFactoryName)); - flagMap[klass] = true; - } - } - return matches; - } - } - - function animationRunner(element, animationEvent, className) { - //transcluded directives may sometimes fire an animation using only comment nodes - //best to catch this early on to prevent any animation operations from occurring - var node = element[0]; - if(!node) { - return; - } - - var isSetClassOperation = animationEvent == 'setClass'; - var isClassBased = isSetClassOperation || - animationEvent == 'addClass' || - animationEvent == 'removeClass'; - - var classNameAdd, classNameRemove; - if(angular.isArray(className)) { - classNameAdd = className[0]; - classNameRemove = className[1]; - className = classNameAdd + ' ' + classNameRemove; - } - - var currentClassName = element.attr('class'); - var classes = currentClassName + ' ' + className; - if(!isAnimatableClassName(classes)) { - return; - } - - var beforeComplete = noop, - beforeCancel = [], - before = [], - afterComplete = noop, - afterCancel = [], - after = []; - - var animationLookup = (' ' + classes).replace(/\s+/g,'.'); - forEach(lookup(animationLookup), function(animationFactory) { - var created = registerAnimation(animationFactory, animationEvent); - if(!created && isSetClassOperation) { - registerAnimation(animationFactory, 'addClass'); - registerAnimation(animationFactory, 'removeClass'); - } - }); - - function registerAnimation(animationFactory, event) { - var afterFn = animationFactory[event]; - var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)]; - if(afterFn || beforeFn) { - if(event == 'leave') { - beforeFn = afterFn; - //when set as null then animation knows to skip this phase - afterFn = null; - } - after.push({ - event : event, fn : afterFn - }); - before.push({ - event : event, fn : beforeFn - }); - return true; - } - } - - function run(fns, cancellations, allCompleteFn) { - var animations = []; - forEach(fns, function(animation) { - animation.fn && animations.push(animation); - }); - - var count = 0; - function afterAnimationComplete(index) { - if(cancellations) { - (cancellations[index] || noop)(); - if(++count < animations.length) return; - cancellations = null; - } - allCompleteFn(); - } - - //The code below adds directly to the array in order to work with - //both sync and async animations. Sync animations are when the done() - //operation is called right away. DO NOT REFACTOR! - forEach(animations, function(animation, index) { - var progress = function() { - afterAnimationComplete(index); - }; - switch(animation.event) { - case 'setClass': - cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress)); - break; - case 'addClass': - cancellations.push(animation.fn(element, classNameAdd || className, progress)); - break; - case 'removeClass': - cancellations.push(animation.fn(element, classNameRemove || className, progress)); - break; - default: - cancellations.push(animation.fn(element, progress)); - break; - } - }); - - if(cancellations && cancellations.length === 0) { - allCompleteFn(); - } - } - - return { - node : node, - event : animationEvent, - className : className, - isClassBased : isClassBased, - isSetClassOperation : isSetClassOperation, - before : function(allCompleteFn) { - beforeComplete = allCompleteFn; - run(before, beforeCancel, function() { - beforeComplete = noop; - allCompleteFn(); - }); - }, - after : function(allCompleteFn) { - afterComplete = allCompleteFn; - run(after, afterCancel, function() { - afterComplete = noop; - allCompleteFn(); - }); - }, - cancel : function() { - if(beforeCancel) { - forEach(beforeCancel, function(cancelFn) { - (cancelFn || noop)(true); - }); - beforeComplete(true); - } - if(afterCancel) { - forEach(afterCancel, function(cancelFn) { - (cancelFn || noop)(true); - }); - afterComplete(true); - } - } - }; - } - - /** - * @ngdoc service - * @name $animate - * @kind function - * - * @description - * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations. - * When any of these operations are run, the $animate service - * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) - * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. - * - * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives - * will work out of the box without any extra configuration. - * - * Requires the {@link ngAnimate `ngAnimate`} module to be installed. - * - * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. - * - */ - return { - /** - * @ngdoc method - * @name $animate#enter - * @kind function - * - * @description - * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once - * the animation is started, the following CSS classes will be present on the element for the duration of the animation: - * - * Below is a breakdown of each step that occurs during enter animation: - * - * | Animation Step | What the element class attribute looks like | - * |-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| - * | 1. $animate.enter(...) is called | class="my-animation" | - * | 2. element is inserted into the parentElement element or beside the afterElement element | class="my-animation" | - * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | - * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | - * | 5. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | - * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | - * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-enter class styling is applied right away | class="my-animation ng-animate ng-enter" | - * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-enter" | - * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-enter" | - * | 10. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-enter ng-enter-active" | - * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-enter ng-enter-active" | - * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 13. The doneCallback() callback is fired (if provided) | class="my-animation" | - * - * @param {DOMElement} element the element that will be the focus of the enter animation - * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation - * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete - * @return {function} the animation cancellation function - */ - enter : function(element, parentElement, afterElement, doneCallback) { - element = angular.element(element); - parentElement = prepareElement(parentElement); - afterElement = prepareElement(afterElement); - - blockElementAnimations(element); - $delegate.enter(element, parentElement, afterElement); - return runAnimationPostDigest(function() { - return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, doneCallback); - }); - }, - - /** - * @ngdoc method - * @name $animate#leave - * @kind function - * - * @description - * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once - * the animation is started, the following CSS classes will be added for the duration of the animation: - * - * Below is a breakdown of each step that occurs during leave animation: - * - * | Animation Step | What the element class attribute looks like | - * |-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| - * | 1. $animate.leave(...) is called | class="my-animation" | - * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | - * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | - * | 4. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | - * | 6. $animate blocks all CSS transitions on the element to ensure the .ng-leave class styling is applied right away | class="my-animation ng-animate ng-leave” | - * | 7. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-leave" | - * | 8. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-leave” | - * | 9. the .ng-leave-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-leave ng-leave-active" | - * | 10. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-leave ng-leave-active" | - * | 11. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 12. The element is removed from the DOM | ... | - * | 13. The doneCallback() callback is fired (if provided) | ... | - * - * @param {DOMElement} element the element that will be the focus of the leave animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete - * @return {function} the animation cancellation function - */ - leave : function(element, doneCallback) { - element = angular.element(element); - - cancelChildAnimations(element); - blockElementAnimations(element); - this.enabled(false, element); - return runAnimationPostDigest(function() { - return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { - $delegate.leave(element); - }, doneCallback); - }); - }, - - /** - * @ngdoc method - * @name $animate#move - * @kind function - * - * @description - * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or - * add the element directly after the afterElement element if present. Then the move animation will be run. Once - * the animation is started, the following CSS classes will be added for the duration of the animation: - * - * Below is a breakdown of each step that occurs during move animation: - * - * | Animation Step | What the element class attribute looks like | - * |------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| - * | 1. $animate.move(...) is called | class="my-animation" | - * | 2. element is moved into the parentElement element or beside the afterElement element | class="my-animation" | - * | 3. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | - * | 4. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | - * | 5. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | - * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | - * | 7. $animate blocks all CSS transitions on the element to ensure the .ng-move class styling is applied right away | class="my-animation ng-animate ng-move” | - * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate ng-move" | - * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate ng-move” | - * | 10. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-move ng-move-active" | - * | 11. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate ng-move ng-move-active" | - * | 12. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 13. The doneCallback() callback is fired (if provided) | class="my-animation" | - * - * @param {DOMElement} element the element that will be the focus of the move animation - * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation - * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation - * @param {function()=} doneCallback the callback function that will be called once the animation is complete - * @return {function} the animation cancellation function - */ - move : function(element, parentElement, afterElement, doneCallback) { - element = angular.element(element); - parentElement = prepareElement(parentElement); - afterElement = prepareElement(afterElement); - - cancelChildAnimations(element); - blockElementAnimations(element); - $delegate.move(element, parentElement, afterElement); - return runAnimationPostDigest(function() { - return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, doneCallback); - }); - }, - - /** - * @ngdoc method - * @name $animate#addClass - * - * @description - * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. - * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide - * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions - * or keyframes are defined on the -add-active or base CSS class). - * - * Below is a breakdown of each step that occurs during addClass animation: - * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------------|------------------------------------------------------------------| - * | 1. $animate.addClass(element, 'super') is called | class="my-animation" | - * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | - * | 3. the .super-add class is added to the element | class="my-animation ng-animate super-add" | - * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate super-add" | - * | 5. the .super and .super-add-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate super super-add super-add-active" | - * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | - * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation super super-add super-add-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation super" | - * | 9. The super class is kept on the element | class="my-animation super" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-animation super" | - * - * @param {DOMElement} element the element that will be animated - * @param {string} className the CSS class that will be added to the element and then animated - * @param {function()=} doneCallback the callback function that will be called once the animation is complete - * @return {function} the animation cancellation function - */ - addClass : function(element, className, doneCallback) { - element = angular.element(element); - element = stripCommentsFromElement(element); - return performAnimation('addClass', className, element, null, null, function() { - $delegate.addClass(element, className); - }, doneCallback); - }, - - /** - * @ngdoc method - * @name $animate#removeClass - * - * @description - * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value - * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in - * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if - * no CSS transitions or keyframes are defined on the -remove or base CSS classes). - * - * Below is a breakdown of each step that occurs during removeClass animation: - * - * | Animation Step | What the element class attribute looks like | - * |------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------| - * | 1. $animate.removeClass(element, 'super') is called | class="my-animation super" | - * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate" | - * | 3. the .super-remove class is added to the element | class="my-animation super ng-animate super-remove" | - * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation super ng-animate super-remove" | - * | 5. the .super-remove-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate super-remove super-remove-active" | - * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | - * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate super-remove super-remove-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" | - * - * - * @param {DOMElement} element the element that will be animated - * @param {string} className the CSS class that will be animated and then removed from the element - * @param {function()=} doneCallback the callback function that will be called once the animation is complete - * @return {function} the animation cancellation function - */ - removeClass : function(element, className, doneCallback) { - element = angular.element(element); - element = stripCommentsFromElement(element); - return performAnimation('removeClass', className, element, null, null, function() { - $delegate.removeClass(element, className); - }, doneCallback); - }, - - /** - * - * @ngdoc method - * @name $animate#setClass - * - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * - * | Animation Step | What the element class attribute looks like | - * |--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| - * | 1. $animate.removeClass(element, ‘on’, ‘off’) is called | class="my-animation super off” | - * | 2. $animate runs the JavaScript-defined animations detected on the element | class="my-animation super ng-animate off” | - * | 3. the .on-add and .off-remove classes are added to the element | class="my-animation ng-animate on-add off-remove off” | - * | 4. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate on-add off-remove off” | - * | 5. the .on, .on-add-active and .off-remove-active classes are added and .off is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active” | - * | 6. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" | - * | 7. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The doneCallback() callback is fired (if provided) | class="my-animation" | - * - * @param {DOMElement} element the element which will have its CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element - * @return {function} the animation cancellation function - */ - setClass : function(element, add, remove, doneCallback) { - element = angular.element(element); - element = stripCommentsFromElement(element); - return performAnimation('setClass', [add, remove], element, null, null, function() { - $delegate.setClass(element, add, remove); - }, doneCallback); - }, - - /** - * @ngdoc method - * @name $animate#enabled - * @kind function - * - * @param {boolean=} value If provided then set the animation on or off. - * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation - * @return {boolean} Current animation state. - * - * @description - * Globally enables/disables animations. - * - */ - enabled : function(value, element) { - switch(arguments.length) { - case 2: - if(value) { - cleanup(element); - } else { - var data = element.data(NG_ANIMATE_STATE) || {}; - data.disabled = true; - element.data(NG_ANIMATE_STATE, data); - } - break; - - case 1: - rootAnimateState.disabled = !value; - break; - - default: - value = !rootAnimateState.disabled; - break; - } - return !!value; - } - }; - - /* - all animations call this shared animation triggering function internally. - The animationEvent variable refers to the JavaScript animation event that will be triggered - and the className value is the name of the animation that will be applied within the - CSS code. Element, parentElement and afterElement are provided DOM elements for the animation - and the onComplete callback will be fired once the animation is fully complete. - */ - function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - - var noopCancel = noop; - var runner = animationRunner(element, animationEvent, className); - if(!runner) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - closeAnimation(); - return noopCancel; - } - - className = runner.className; - var elementEvents = angular.element._data(runner.node); - elementEvents = elementEvents && elementEvents.events; - - if (!parentElement) { - parentElement = afterElement ? afterElement.parent() : element.parent(); - } - - var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; - var totalActiveAnimations = ngAnimateState.totalActive || 0; - var lastAnimation = ngAnimateState.last; - - //only allow animations if the currently running animation is not structural - //or if there is no animation running at all - var skipAnimations; - if (runner.isClassBased) { - skipAnimations = ngAnimateState.running || - ngAnimateState.disabled || - (lastAnimation && !lastAnimation.isClassBased); - } - - //skip the animation if animations are disabled, a parent is already being animated, - //the element is not currently attached to the document body or then completely close - //the animation if any matching animations are not found at all. - //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found. - if (skipAnimations || animationsDisabled(element, parentElement)) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - closeAnimation(); - return noopCancel; - } - - var skipAnimation = false; - if(totalActiveAnimations > 0) { - var animationsToCancel = []; - if(!runner.isClassBased) { - if(animationEvent == 'leave' && runningAnimations['ng-leave']) { - skipAnimation = true; - } else { - //cancel all animations when a structural animation takes place - for(var klass in runningAnimations) { - animationsToCancel.push(runningAnimations[klass]); - } - ngAnimateState = {}; - cleanup(element, true); - } - } else if(lastAnimation.event == 'setClass') { - animationsToCancel.push(lastAnimation); - cleanup(element, className); - } - else if(runningAnimations[className]) { - var current = runningAnimations[className]; - if(current.event == animationEvent) { - skipAnimation = true; - } else { - animationsToCancel.push(current); - cleanup(element, className); - } - } - - if(animationsToCancel.length > 0) { - forEach(animationsToCancel, function(operation) { - operation.cancel(); - }); - } - } - - runningAnimations = ngAnimateState.active || {}; - totalActiveAnimations = ngAnimateState.totalActive || 0; - - if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { - skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR - } - - if(skipAnimation) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - fireDoneCallbackAsync(); - return noopCancel; - } - - if(animationEvent == 'leave') { - //there's no need to ever remove the listener since the element - //will be removed (destroyed) after the leave animation ends or - //is cancelled midway - element.one('$destroy', function(e) { - var element = angular.element(this); - var state = element.data(NG_ANIMATE_STATE); - if(state) { - var activeLeaveAnimation = state.active['ng-leave']; - if(activeLeaveAnimation) { - activeLeaveAnimation.cancel(); - cleanup(element, 'ng-leave'); - } - } - }); - } - - //the ng-animate class does nothing, but it's here to allow for - //parent animations to find and cancel child animations when needed - element.addClass(NG_ANIMATE_CLASS_NAME); - - var localAnimationCount = globalAnimationCounter++; - totalActiveAnimations++; - runningAnimations[className] = runner; - - element.data(NG_ANIMATE_STATE, { - last : runner, - active : runningAnimations, - index : localAnimationCount, - totalActive : totalActiveAnimations - }); - - //first we run the before animations and when all of those are complete - //then we perform the DOM operation and run the next set of animations - fireBeforeCallbackAsync(); - runner.before(function(cancelled) { - var data = element.data(NG_ANIMATE_STATE); - cancelled = cancelled || - !data || !data.active[className] || - (runner.isClassBased && data.active[className].event != animationEvent); - - fireDOMOperation(); - if(cancelled === true) { - closeAnimation(); - } else { - fireAfterCallbackAsync(); - runner.after(closeAnimation); - } - }); - - return runner.cancel; - - function fireDOMCallback(animationPhase) { - var eventName = '$animate:' + animationPhase; - if(elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { - $$asyncCallback(function() { - element.triggerHandler(eventName, { - event : animationEvent, - className : className - }); - }); - } - } - - function fireBeforeCallbackAsync() { - fireDOMCallback('before'); - } - - function fireAfterCallbackAsync() { - fireDOMCallback('after'); - } - - function fireDoneCallbackAsync() { - fireDOMCallback('close'); - if(doneCallback) { - $$asyncCallback(function() { - doneCallback(); - }); - } - } - - //it is less complicated to use a flag than managing and canceling - //timeouts containing multiple callbacks. - function fireDOMOperation() { - if(!fireDOMOperation.hasBeenRun) { - fireDOMOperation.hasBeenRun = true; - domOperation(); - } - } - - function closeAnimation() { - if(!closeAnimation.hasBeenRun) { - closeAnimation.hasBeenRun = true; - var data = element.data(NG_ANIMATE_STATE); - if(data) { - /* only structural animations wait for reflow before removing an - animation, but class-based animations don't. An example of this - failing would be when a parent HTML tag has a ng-class attribute - causing ALL directives below to skip animations during the digest */ - if(runner && runner.isClassBased) { - cleanup(element, className); - } else { - $$asyncCallback(function() { - var data = element.data(NG_ANIMATE_STATE) || {}; - if(localAnimationCount == data.index) { - cleanup(element, className, animationEvent); - } - }); - element.data(NG_ANIMATE_STATE, data); - } - } - fireDoneCallbackAsync(); - } - } - } - - function cancelChildAnimations(element) { - var node = extractElementNode(element); - if (node) { - var nodes = angular.isFunction(node.getElementsByClassName) ? - node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) : - node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME); - forEach(nodes, function(element) { - element = angular.element(element); - var data = element.data(NG_ANIMATE_STATE); - if(data && data.active) { - forEach(data.active, function(runner) { - runner.cancel(); - }); - } - }); - } - } - - function cleanup(element, className) { - if(isMatchingElement(element, $rootElement)) { - if(!rootAnimateState.disabled) { - rootAnimateState.running = false; - rootAnimateState.structural = false; - } - } else if(className) { - var data = element.data(NG_ANIMATE_STATE) || {}; - - var removeAnimations = className === true; - if(!removeAnimations && data.active && data.active[className]) { - data.totalActive--; - delete data.active[className]; - } - - if(removeAnimations || !data.totalActive) { - element.removeClass(NG_ANIMATE_CLASS_NAME); - element.removeData(NG_ANIMATE_STATE); - } - } - } - - function animationsDisabled(element, parentElement) { - if (rootAnimateState.disabled) { - return true; - } - - if (isMatchingElement(element, $rootElement)) { - return rootAnimateState.running; - } - - var allowChildAnimations, parentRunningAnimation, hasParent; - do { - //the element did not reach the root element which means that it - //is not apart of the DOM. Therefore there is no reason to do - //any animations on it - if (parentElement.length === 0) break; - - var isRoot = isMatchingElement(parentElement, $rootElement); - var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {}); - if (state.disabled) { - return true; - } - - //no matter what, for an animation to work it must reach the root element - //this implies that the element is attached to the DOM when the animation is run - if (isRoot) { - hasParent = true; - } - - //once a flag is found that is strictly false then everything before - //it will be discarded and all child animations will be restricted - if (allowChildAnimations !== false) { - var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN); - if(angular.isDefined(animateChildrenFlag)) { - allowChildAnimations = animateChildrenFlag; - } - } - - parentRunningAnimation = parentRunningAnimation || - state.running || - (state.last && !state.last.isClassBased); - } - while(parentElement = parentElement.parent()); - - return !hasParent || (!allowChildAnimations && parentRunningAnimation); - } - }]); - - $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow', - function($window, $sniffer, $timeout, $$animateReflow) { - // Detect proper transitionend/animationend event names. - var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; - - // If unprefixed events are not supported but webkit-prefixed are, use the latter. - // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. - // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` - // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. - // Register both events in case `window.onanimationend` is not supported because of that, - // do the same for `transitionend` as Safari is likely to exhibit similar behavior. - // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit - // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition - if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { - CSS_PREFIX = '-webkit-'; - TRANSITION_PROP = 'WebkitTransition'; - TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; - } else { - TRANSITION_PROP = 'transition'; - TRANSITIONEND_EVENT = 'transitionend'; - } - - if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { - CSS_PREFIX = '-webkit-'; - ANIMATION_PROP = 'WebkitAnimation'; - ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; - } else { - ANIMATION_PROP = 'animation'; - ANIMATIONEND_EVENT = 'animationend'; - } - - var DURATION_KEY = 'Duration'; - var PROPERTY_KEY = 'Property'; - var DELAY_KEY = 'Delay'; - var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; - var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; - var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; - var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; - var CLOSING_TIME_BUFFER = 1.5; - var ONE_SECOND = 1000; - - var lookupCache = {}; - var parentCounter = 0; - var animationReflowQueue = []; - var cancelAnimationReflow; - function afterReflow(element, callback) { - if(cancelAnimationReflow) { - cancelAnimationReflow(); - } - animationReflowQueue.push(callback); - cancelAnimationReflow = $$animateReflow(function() { - forEach(animationReflowQueue, function(fn) { - fn(); - }); - - animationReflowQueue = []; - cancelAnimationReflow = null; - lookupCache = {}; - }); - } - - var closingTimer = null; - var closingTimestamp = 0; - var animationElementQueue = []; - function animationCloseHandler(element, totalTime) { - var node = extractElementNode(element); - element = angular.element(node); - - //this item will be garbage collected by the closing - //animation timeout - animationElementQueue.push(element); - - //but it may not need to cancel out the existing timeout - //if the timestamp is less than the previous one - var futureTimestamp = Date.now() + totalTime; - if(futureTimestamp <= closingTimestamp) { - return; - } - - $timeout.cancel(closingTimer); - - closingTimestamp = futureTimestamp; - closingTimer = $timeout(function() { - closeAllAnimations(animationElementQueue); - animationElementQueue = []; - }, totalTime, false); - } - - function closeAllAnimations(elements) { - forEach(elements, function(element) { - var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(elementData) { - forEach(elementData.closeAnimationFns, function(fn) { - fn(); - }); - } - }); - } - - function getElementAnimationDetails(element, cacheKey) { - var data = cacheKey ? lookupCache[cacheKey] : null; - if(!data) { - var transitionDuration = 0; - var transitionDelay = 0; - var animationDuration = 0; - var animationDelay = 0; - var transitionDelayStyle; - var animationDelayStyle; - var transitionDurationStyle; - var transitionPropertyStyle; - - //we want all the styles defined before and after - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; - - transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - - transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); - - transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; - - transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - - transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); - - animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; - - animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); - - var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); - - if(aDuration > 0) { - aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; - } - - animationDuration = Math.max(aDuration, animationDuration); - } - }); - data = { - total : 0, - transitionPropertyStyle: transitionPropertyStyle, - transitionDurationStyle: transitionDurationStyle, - transitionDelayStyle: transitionDelayStyle, - transitionDelay: transitionDelay, - transitionDuration: transitionDuration, - animationDelayStyle: animationDelayStyle, - animationDelay: animationDelay, - animationDuration: animationDuration - }; - if(cacheKey) { - lookupCache[cacheKey] = data; - } - } - return data; - } - - function parseMaxTime(str) { - var maxValue = 0; - var values = angular.isString(str) ? - str.split(/\s*,\s*/) : - []; - forEach(values, function(value) { - maxValue = Math.max(parseFloat(value) || 0, maxValue); - }); - return maxValue; - } - - function getCacheKey(element) { - var parentElement = element.parent(); - var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); - if(!parentID) { - parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); - parentID = parentCounter; - } - return parentID + '-' + extractElementNode(element).getAttribute('class'); - } - - function animateSetup(animationEvent, element, className) { - var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0; - - var cacheKey = getCacheKey(element); - var eventCacheKey = cacheKey + ' ' + className; - var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; - - var stagger = {}; - if(itemIndex > 0) { - var staggerClassName = className + '-stagger'; - var staggerCacheKey = cacheKey + ' ' + staggerClassName; - var applyClasses = !lookupCache[staggerCacheKey]; - - applyClasses && element.addClass(staggerClassName); - - stagger = getElementAnimationDetails(element, staggerCacheKey); - - applyClasses && element.removeClass(staggerClassName); - } - - element.addClass(className); - - var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; - var timings = getElementAnimationDetails(element, eventCacheKey); - var transitionDuration = timings.transitionDuration; - var animationDuration = timings.animationDuration; - - if(structural && transitionDuration === 0 && animationDuration === 0) { - element.removeClass(className); - return false; - } - - var blockTransition = structural && transitionDuration > 0; - var blockAnimation = animationDuration > 0 && - stagger.animationDelay > 0 && - stagger.animationDuration === 0; - - var closeAnimationFns = formerData.closeAnimationFns || []; - element.data(NG_ANIMATE_CSS_DATA_KEY, { - stagger : stagger, - cacheKey : eventCacheKey, - running : formerData.running || 0, - itemIndex : itemIndex, - blockTransition : blockTransition, - blockAnimation : blockAnimation, - closeAnimationFns : closeAnimationFns - }); - - var node = extractElementNode(element); - - if(blockTransition) { - node.style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; - } - - if(blockAnimation) { - node.style[ANIMATION_PROP] = 'none 0s'; - } - - return true; - } - - function animateRun(animationEvent, element, className, activeAnimationComplete) { - var node = extractElementNode(element); - var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(node.getAttribute('class').indexOf(className) == -1 || !elementData) { - activeAnimationComplete(); - return; - } - - if(elementData.blockTransition) { - node.style[TRANSITION_PROP + PROPERTY_KEY] = ''; - } - - if(elementData.blockAnimation) { - node.style[ANIMATION_PROP] = ''; - } - - var activeClassName = ''; - forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; - }); - - element.addClass(activeClassName); - var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; - var timings = getElementAnimationDetails(element, eventCacheKey); - - var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); - if(maxDuration === 0) { - element.removeClass(activeClassName); - animateClose(element, className); - activeAnimationComplete(); - return; - } - - var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); - var stagger = elementData.stagger; - var itemIndex = elementData.itemIndex; - var maxDelayTime = maxDelay * ONE_SECOND; - - var style = '', appliedStyles = []; - if(timings.transitionDuration > 0) { - var propertyStyle = timings.transitionPropertyStyle; - if(propertyStyle.indexOf('all') == -1) { - style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';'; - style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ';'; - appliedStyles.push(CSS_PREFIX + 'transition-property'); - appliedStyles.push(CSS_PREFIX + 'transition-duration'); - } - } - - if(itemIndex > 0) { - if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { - var delayStyle = timings.transitionDelayStyle; - style += CSS_PREFIX + 'transition-delay: ' + - prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'transition-delay'); - } - - if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { - style += CSS_PREFIX + 'animation-delay: ' + - prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'animation-delay'); - } - } - - if(appliedStyles.length > 0) { - //the element being animated may sometimes contain comment nodes in - //the jqLite object, so we're safe to use a single variable to house - //the styles since there is always only one element being animated - var oldStyle = node.getAttribute('style') || ''; - node.setAttribute('style', oldStyle + '; ' + style); - } - - var startTime = Date.now(); - var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; - - element.on(css3AnimationEvents, onAnimationProgress); - elementData.closeAnimationFns.push(function() { - onEnd(); - activeAnimationComplete(); - }); - - var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); - var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; - var totalTime = (staggerTime + animationTime) * ONE_SECOND; - - elementData.running++; - animationCloseHandler(element, totalTime); - return onEnd; - - // This will automatically be called by $animate so - // there is no need to attach this internally to the - // timeout done method. - function onEnd(cancelled) { - element.off(css3AnimationEvents, onAnimationProgress); - element.removeClass(activeClassName); - animateClose(element, className); - var node = extractElementNode(element); - for (var i in appliedStyles) { - node.style.removeProperty(appliedStyles[i]); - } - } - - function onAnimationProgress(event) { - event.stopPropagation(); - var ev = event.originalEvent || event; - var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); - - /* Firefox (or possibly just Gecko) likes to not round values up - * when a ms measurement is used for the animation */ - var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); - - /* $manualTimeStamp is a mocked timeStamp value which is set - * within browserTrigger(). This is only here so that tests can - * mock animations properly. Real events fallback to event.timeStamp, - * or, if they don't, then a timeStamp is automatically created for them. - * We're checking to see if the timeStamp surpasses the expected delay, - * but we're using elapsedTime instead of the timeStamp on the 2nd - * pre-condition since animations sometimes close off early */ - if(Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { - activeAnimationComplete(); - } - } - } - - function prepareStaggerDelay(delayStyle, staggerDelay, index) { - var style = ''; - forEach(delayStyle.split(','), function(val, i) { - style += (i > 0 ? ',' : '') + - (index * staggerDelay + parseInt(val, 10)) + 's'; - }); - return style; - } - - function animateBefore(animationEvent, element, className, calculationDecorator) { - if(animateSetup(animationEvent, element, className, calculationDecorator)) { - return function(cancelled) { - cancelled && animateClose(element, className); - }; - } - } - - function animateAfter(animationEvent, element, className, afterAnimationComplete) { - if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { - return animateRun(animationEvent, element, className, afterAnimationComplete); - } else { - animateClose(element, className); - afterAnimationComplete(); - } - } - - function animate(animationEvent, element, className, animationComplete) { - //If the animateSetup function doesn't bother returning a - //cancellation function then it means that there is no animation - //to perform at all - var preReflowCancellation = animateBefore(animationEvent, element, className); - if(!preReflowCancellation) { - animationComplete(); - return; - } - - //There are two cancellation functions: one is before the first - //reflow animation and the second is during the active state - //animation. The first function will take care of removing the - //data from the element which will not make the 2nd animation - //happen in the first place - var cancel = preReflowCancellation; - afterReflow(element, function() { - //once the reflow is complete then we point cancel to - //the new cancellation function which will remove all of the - //animation properties from the active animation - cancel = animateAfter(animationEvent, element, className, animationComplete); - }); - - return function(cancelled) { - (cancel || noop)(cancelled); - }; - } - - function animateClose(element, className) { - element.removeClass(className); - var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(data) { - if(data.running) { - data.running--; - } - if(!data.running || data.running === 0) { - element.removeData(NG_ANIMATE_CSS_DATA_KEY); - } - } - } - - return { - enter : function(element, animationCompleted) { - return animate('enter', element, 'ng-enter', animationCompleted); - }, - - leave : function(element, animationCompleted) { - return animate('leave', element, 'ng-leave', animationCompleted); - }, - - move : function(element, animationCompleted) { - return animate('move', element, 'ng-move', animationCompleted); - }, - - beforeSetClass : function(element, add, remove, animationCompleted) { - var className = suffixClasses(remove, '-remove') + ' ' + - suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className); - if(cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; - } - animationCompleted(); - }, - - beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add')); - if(cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; - } - animationCompleted(); - }, - - beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove')); - if(cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; - } - animationCompleted(); - }, - - setClass : function(element, add, remove, animationCompleted) { - remove = suffixClasses(remove, '-remove'); - add = suffixClasses(add, '-add'); - var className = remove + ' ' + add; - return animateAfter('setClass', element, className, animationCompleted); - }, - - addClass : function(element, className, animationCompleted) { - return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted); - }, - - removeClass : function(element, className, animationCompleted) { - return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); - } - }; - - function suffixClasses(classes, suffix) { - var className = ''; - classes = angular.isArray(classes) ? classes : classes.split(/\s+/); - forEach(classes, function(klass, i) { - if(klass && klass.length > 0) { - className += (i > 0 ? ' ' : '') + klass + suffix; - } - }); - return className; - } - }]); - }]); - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-animate.min.js b/src/main/webapp/new/lib/angular/angular-animate.min.js deleted file mode 100644 index 942fc5ea..00000000 --- a/src/main/webapp/new/lib/angular/angular-animate.min.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(J,e,P){'use strict';e.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(K,v,f){f=f.ngAnimateChildren;e.isString(f)&&0===f.length?v.data("$$ngAnimateChildren",!0):K.$watch(f,function(e){v.data("$$ngAnimateChildren",!!e)})}}).factory("$$animateReflow",["$$rAF","$document",function(e,v){return function(f){return e(function(){f()})}}]).config(["$provide","$animateProvider",function(K,v){function f(e){for(var f=0;f=B&&d>=w&&h()}var q=f(d);k=d.data(u);if(-1!=q.getAttribute("class").indexOf(e)&&k){k.blockTransition&&(q.style[a+V]="");k.blockAnimation&&(q.style[c]="");var t="";m(e.split(" "),function(a,d){t+=(0 - * - * See {@link ngCookies.$cookies `$cookies`} and - * {@link ngCookies.$cookieStore `$cookieStore`} for usage. - */ - - -angular.module('ngCookies', ['ng']). - /** - * @ngdoc service - * @name $cookies - * - * @description - * Provides read/write access to browser's cookies. - * - * Only a simple Object is exposed and by adding or removing properties to/from this object, new - * cookies are created/deleted at the end of current $eval. - * The object's properties can only be strings. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookiesExample', ['ngCookies']) - * .controller('ExampleController', ['$cookies', function($cookies) { - * // Retrieving a cookie - * var favoriteCookie = $cookies.myFavorite; - * // Setting a cookie - * $cookies.myFavorite = 'oatmeal'; - * }]); - * ``` - */ - factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { - var cookies = {}, - lastCookies = {}, - lastBrowserCookies, - runEval = false, - copy = angular.copy, - isUndefined = angular.isUndefined; - - //creates a poller fn that copies all cookies from the $browser to service & inits the service - $browser.addPollFn(function() { - var currentCookies = $browser.cookies(); - if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl - lastBrowserCookies = currentCookies; - copy(currentCookies, lastCookies); - copy(currentCookies, cookies); - if (runEval) $rootScope.$apply(); - } - })(); - - runEval = true; - - //at the end of each eval, push cookies - //TODO: this should happen before the "delayed" watches fire, because if some cookies are not - // strings or browser refuses to store some cookies, we update the model in the push fn. - $rootScope.$watch(push); - - return cookies; - - - /** - * Pushes all the cookies from the service to the browser and verifies if all cookies were - * stored. - */ - function push() { - var name, - value, - browserCookies, - updated; - - //delete any cookies deleted in $cookies - for (name in lastCookies) { - if (isUndefined(cookies[name])) { - $browser.cookies(name, undefined); - } - } - - //update all cookies updated in $cookies - for(name in cookies) { - value = cookies[name]; - if (!angular.isString(value)) { - value = '' + value; - cookies[name] = value; - } - if (value !== lastCookies[name]) { - $browser.cookies(name, value); - updated = true; - } - } - - //verify what was actually stored - if (updated){ - updated = false; - browserCookies = $browser.cookies(); - - for (name in cookies) { - if (cookies[name] !== browserCookies[name]) { - //delete or reset all cookies that the browser dropped from $cookies - if (isUndefined(browserCookies[name])) { - delete cookies[name]; - } else { - cookies[name] = browserCookies[name]; - } - updated = true; - } - } - } - } - }]). - - - /** - * @ngdoc service - * @name $cookieStore - * @requires $cookies - * - * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookieStoreExample', ['ngCookies']) - * .controller('ExampleController', ['$cookieStore', function($cookieStore) { - * // Put cookie - * $cookieStore.put('myFavorite','oatmeal'); - * // Get cookie - * var favoriteCookie = $cookieStore.get('myFavorite'); - * // Removing a cookie - * $cookieStore.remove('myFavorite'); - * }]); - * ``` - */ - factory('$cookieStore', ['$cookies', function($cookies) { - - return { - /** - * @ngdoc method - * @name $cookieStore#get - * - * @description - * Returns the value of given cookie key - * - * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value. - */ - get: function(key) { - var value = $cookies[key]; - return value ? angular.fromJson(value) : value; - }, - - /** - * @ngdoc method - * @name $cookieStore#put - * - * @description - * Sets a value for given cookie key - * - * @param {string} key Id for the `value`. - * @param {Object} value Value to be stored. - */ - put: function(key, value) { - $cookies[key] = angular.toJson(value); - }, - - /** - * @ngdoc method - * @name $cookieStore#remove - * - * @description - * Remove given cookie - * - * @param {string} key Id of the key-value pair to delete. - */ - remove: function(key) { - delete $cookies[key]; - } - }; - - }]); - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-cookies.min.js b/src/main/webapp/new/lib/angular/angular-cookies.min.js deleted file mode 100644 index 1be39294..00000000 --- a/src/main/webapp/new/lib/angular/angular-cookies.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(e,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&e.$apply())})();k=!0;e.$watch(function(){var a,d,e;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)d=c[a],f.isString(d)||(d=""+d,c[a]=d),d!==g[a]&&(b.cookies(a,d),e=!0);if(e)for(a in d=b.cookies(),c)c[a]!==d[a]&&(m(d[a])?delete c[a]:c[a]=d[a])});return c}]).factory("$cookieStore", -["$cookies",function(e){return{get:function(b){return(b=e[b])?f.fromJson(b):b},put:function(b,c){e[b]=f.toJson(c)},remove:function(b){delete e[b]}}}])})(window,window.angular); -//# sourceMappingURL=angular-cookies.min.js.map diff --git a/src/main/webapp/new/lib/angular/angular-cookies.min.js.map b/src/main/webapp/new/lib/angular/angular-cookies.min.js.map deleted file mode 100644 index 26f36168..00000000 --- a/src/main/webapp/new/lib/angular/angular-cookies.min.js.map +++ /dev/null @@ -1,8 +0,0 @@ -{ -"version":3, -"file":"angular-cookies.min.js", -"lineCount":7, -"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAmBtCD,CAAAE,OAAA,CAAe,WAAf,CAA4B,CAAC,IAAD,CAA5B,CAAAC,QAAA,CA0BW,UA1BX,CA0BuB,CAAC,YAAD,CAAe,UAAf,CAA2B,QAAS,CAACC,CAAD,CAAaC,CAAb,CAAuB,CAAA,IACxEC,EAAU,EAD8D,CAExEC,EAAc,EAF0D,CAGxEC,CAHwE,CAIxEC,EAAU,CAAA,CAJ8D,CAKxEC,EAAOV,CAAAU,KALiE,CAMxEC,EAAcX,CAAAW,YAGlBN,EAAAO,UAAA,CAAmB,QAAQ,EAAG,CAC5B,IAAIC,EAAiBR,CAAAC,QAAA,EACjBE,EAAJ,EAA0BK,CAA1B,GACEL,CAGA,CAHqBK,CAGrB,CAFAH,CAAA,CAAKG,CAAL,CAAqBN,CAArB,CAEA,CADAG,CAAA,CAAKG,CAAL,CAAqBP,CAArB,CACA,CAAIG,CAAJ,EAAaL,CAAAU,OAAA,EAJf,CAF4B,CAA9B,CAAA,EAUAL,EAAA,CAAU,CAAA,CAKVL,EAAAW,OAAA,CASAC,QAAa,EAAG,CAAA,IACVC,CADU,CAEVC,CAFU,CAIVC,CAGJ,KAAKF,CAAL,GAAaV,EAAb,CACMI,CAAA,CAAYL,CAAA,CAAQW,CAAR,CAAZ,CAAJ,EACEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBhB,CAAvB,CAKJ,KAAIgB,CAAJ,GAAYX,EAAZ,CACEY,CAKA,CALQZ,CAAA,CAAQW,CAAR,CAKR,CAJKjB,CAAAoB,SAAA,CAAiBF,CAAjB,CAIL,GAHEA,CACA,CADQ,EACR,CADaA,CACb,CAAAZ,CAAA,CAAQW,CAAR,CAAA,CAAgBC,CAElB,EAAIA,CAAJ,GAAcX,CAAA,CAAYU,CAAZ,CAAd,GACEZ,CAAAC,QAAA,CAAiBW,CAAjB,CAAuBC,CAAvB,CACA,CAAAC,CAAA,CAAU,CAAA,CAFZ,CAOF,IAAIA,CAAJ,CAIE,IAAKF,CAAL,GAFAI,EAEaf,CAFID,CAAAC,QAAA,EAEJA,CAAAA,CAAb,CACMA,CAAA,CAAQW,CAAR,CAAJ,GAAsBI,CAAA,CAAeJ,CAAf,CAAtB,GAEMN,CAAA,CAAYU,CAAA,CAAeJ,CAAf,CAAZ,CAAJ,CACE,OAAOX,CAAA,CAAQW,CAAR,CADT,CAGEX,CAAA,CAAQW,CAAR,CAHF,CAGkBI,CAAA,CAAeJ,CAAf,CALpB,CAhCU,CAThB,CAEA,OAAOX,EA1BqE,CAA3D,CA1BvB,CAAAH,QAAA,CAoIW,cApIX;AAoI2B,CAAC,UAAD,CAAa,QAAQ,CAACmB,CAAD,CAAW,CAErD,MAAO,KAWAC,QAAQ,CAACC,CAAD,CAAM,CAEjB,MAAO,CADHN,CACG,CADKI,CAAA,CAASE,CAAT,CACL,EAAQxB,CAAAyB,SAAA,CAAiBP,CAAjB,CAAR,CAAkCA,CAFxB,CAXd,KA0BAQ,QAAQ,CAACF,CAAD,CAAMN,CAAN,CAAa,CACxBI,CAAA,CAASE,CAAT,CAAA,CAAgBxB,CAAA2B,OAAA,CAAeT,CAAf,CADQ,CA1BrB,QAuCGU,QAAQ,CAACJ,CAAD,CAAM,CACpB,OAAOF,CAAA,CAASE,CAAT,CADa,CAvCjB,CAF8C,CAAhC,CApI3B,CAnBsC,CAArC,CAAA,CAwMEzB,MAxMF,CAwMUA,MAAAC,QAxMV;", -"sources":["angular-cookies.js"], -"names":["window","angular","undefined","module","factory","$rootScope","$browser","cookies","lastCookies","lastBrowserCookies","runEval","copy","isUndefined","addPollFn","currentCookies","$apply","$watch","push","name","value","updated","isString","browserCookies","$cookies","get","key","fromJson","put","toJson","remove"] -} diff --git a/src/main/webapp/new/lib/angular/angular-csp.css b/src/main/webapp/new/lib/angular/angular-csp.css deleted file mode 100644 index 21245402..00000000 --- a/src/main/webapp/new/lib/angular/angular-csp.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Include this file in your html if you are using the CSP mode. */ - -@charset "UTF-8"; - -[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], -.ng-cloak, .x-ng-cloak, -.ng-hide:not(.ng-animate) { - display: none !important; -} - -ng\:form { - display: block; -} diff --git a/src/main/webapp/new/lib/angular/angular-loader.js b/src/main/webapp/new/lib/angular/angular-loader.js deleted file mode 100644 index f292b680..00000000 --- a/src/main/webapp/new/lib/angular/angular-loader.js +++ /dev/null @@ -1,419 +0,0 @@ -/** - * @license AngularJS v1.3.0-beta.17 - * (c) 2010-2014 Google, Inc. http://angularjs.org - * License: MIT - */ - -(function() {'use strict'; - -/** - * @description - * - * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: - * - * var exampleMinErr = minErr('example'); - * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); - * - * The above creates an instance of minErr in the example namespace. The - * resulting error will have a namespaced error code of example.one. The - * resulting error will replace {0} with the value of foo, and {1} with the - * value of bar. The object is not restricted in the number of arguments it can - * take. - * - * If fewer arguments are specified than necessary for interpolation, the extra - * interpolation markers will be preserved in the final string. - * - * Since data will be parsed statically during a build step, some restrictions - * are applied with respect to how minErr instances are created and called. - * Instances should have names of the form namespaceMinErr for a minErr created - * using minErr('namespace') . Error codes, namespaces and template strings - * should all be static strings, not variables or general expressions. - * - * @param {string} module The namespace to use for the new minErr instance. - * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance - */ - -function minErr(module) { - return function () { - var code = arguments[0], - prefix = '[' + (module ? module + ':' : '') + code + '] ', - template = arguments[1], - templateArgs = arguments, - stringify = function (obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return JSON.stringify(obj); - } - return obj; - }, - message, i; - - message = prefix + template.replace(/\{\d+\}/g, function (match) { - var index = +match.slice(1, -1), arg; - - if (index + 2 < templateArgs.length) { - arg = templateArgs[index + 2]; - if (typeof arg === 'function') { - return arg.toString().replace(/ ?\{[\s\S]*$/, ''); - } else if (typeof arg === 'undefined') { - return 'undefined'; - } else if (typeof arg !== 'string') { - return toJson(arg); - } - return arg; - } - return match; - }); - - message = message + '\nhttp://errors.angularjs.org/1.3.0-beta.17/' + - (module ? module + '/' : '') + code; - for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + - encodeURIComponent(stringify(arguments[i])); - } - - return new Error(message); - }; -} - -/** - * @ngdoc type - * @name angular.Module - * @module ng - * @description - * - * Interface for configuring angular {@link angular.module modules}. - */ - -function setupModuleLoader(window) { - - var $injectorMinErr = minErr('$injector'); - var ngMinErr = minErr('ng'); - - function ensure(obj, name, factory) { - return obj[name] || (obj[name] = factory()); - } - - var angular = ensure(window, 'angular', Object); - - // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap - angular.$$minErr = angular.$$minErr || minErr; - - return ensure(angular, 'module', function() { - /** @type {Object.} */ - var modules = {}; - - /** - * @ngdoc function - * @name angular.module - * @module ng - * @description - * - * The `angular.module` is a global place for creating, registering and retrieving Angular - * modules. - * All modules (angular core or 3rd party) that should be available to an application must be - * registered using this mechanism. - * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. - * - * - * # Module - * - * A module is a collection of services, directives, controllers, filters, and configuration information. - * `angular.module` is used to configure the {@link auto.$injector $injector}. - * - * ```js - * // Create a new module - * var myModule = angular.module('myModule', []); - * - * // register a new service - * myModule.value('appName', 'MyCoolApp'); - * - * // configure existing services inside initialization blocks. - * myModule.config(['$locationProvider', function($locationProvider) { - * // Configure existing providers - * $locationProvider.hashPrefix('!'); - * }]); - * ``` - * - * Then you can create an injector and load your modules like this: - * - * ```js - * var injector = angular.injector(['ng', 'myModule']) - * ``` - * - * However it's more likely that you'll just use - * {@link ng.directive:ngApp ngApp} or - * {@link angular.bootstrap} to simplify this process for you. - * - * @param {!string} name The name of the module to create or retrieve. - * @param {!Array.=} requires If specified then new module is being created. If - * unspecified then the module is being retrieved for further configuration. - * @param {Function=} configFn Optional configuration function for the module. Same as - * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. - */ - return function module(name, requires, configFn) { - var assertNotHasOwnProperty = function(name, context) { - if (name === 'hasOwnProperty') { - throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); - } - }; - - assertNotHasOwnProperty(name, 'module'); - if (requires && modules.hasOwnProperty(name)) { - modules[name] = null; - } - return ensure(modules, name, function() { - if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); - } - - /** @type {!Array.>} */ - var invokeQueue = []; - - /** @type {!Array.} */ - var configBlocks = []; - - /** @type {!Array.} */ - var runBlocks = []; - - var config = invokeLater('$injector', 'invoke', 'push', configBlocks); - - /** @type {angular.Module} */ - var moduleInstance = { - // Private state - _invokeQueue: invokeQueue, - _configBlocks: configBlocks, - _runBlocks: runBlocks, - - /** - * @ngdoc property - * @name angular.Module#requires - * @module ng - * @returns {Array.} List of module names which must be loaded before this module. - * @description - * Holds the list of modules which the injector will load before the current module is - * loaded. - */ - requires: requires, - - /** - * @ngdoc property - * @name angular.Module#name - * @module ng - * @returns {string} Name of the module. - * @description - */ - name: name, - - - /** - * @ngdoc method - * @name angular.Module#provider - * @module ng - * @param {string} name service name - * @param {Function} providerType Construction function for creating new instance of the - * service. - * @description - * See {@link auto.$provide#provider $provide.provider()}. - */ - provider: invokeLater('$provide', 'provider'), - - /** - * @ngdoc method - * @name angular.Module#factory - * @module ng - * @param {string} name service name - * @param {Function} providerFunction Function for creating new instance of the service. - * @description - * See {@link auto.$provide#factory $provide.factory()}. - */ - factory: invokeLater('$provide', 'factory'), - - /** - * @ngdoc method - * @name angular.Module#service - * @module ng - * @param {string} name service name - * @param {Function} constructor A constructor function that will be instantiated. - * @description - * See {@link auto.$provide#service $provide.service()}. - */ - service: invokeLater('$provide', 'service'), - - /** - * @ngdoc method - * @name angular.Module#value - * @module ng - * @param {string} name service name - * @param {*} object Service instance object. - * @description - * See {@link auto.$provide#value $provide.value()}. - */ - value: invokeLater('$provide', 'value'), - - /** - * @ngdoc method - * @name angular.Module#constant - * @module ng - * @param {string} name constant name - * @param {*} object Constant value. - * @description - * Because the constant are fixed, they get applied before other provide methods. - * See {@link auto.$provide#constant $provide.constant()}. - */ - constant: invokeLater('$provide', 'constant', 'unshift'), - - /** - * @ngdoc method - * @name angular.Module#animation - * @module ng - * @param {string} name animation name - * @param {Function} animationFactory Factory function for creating new instance of an - * animation. - * @description - * - * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. - * - * - * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. - * - * ```js - * module.animation('.animation-name', function($inject1, $inject2) { - * return { - * eventName : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction(element) { - * //code to cancel the animation - * } - * } - * } - * }) - * ``` - * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and - * {@link ngAnimate ngAnimate module} for more information. - */ - animation: invokeLater('$animateProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#filter - * @module ng - * @param {string} name Filter name. - * @param {Function} filterFactory Factory function for creating new instance of filter. - * @description - * See {@link ng.$filterProvider#register $filterProvider.register()}. - */ - filter: invokeLater('$filterProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#controller - * @module ng - * @param {string|Object} name Controller name, or an object map of controllers where the - * keys are the names and the values are the constructors. - * @param {Function} constructor Controller constructor function. - * @description - * See {@link ng.$controllerProvider#register $controllerProvider.register()}. - */ - controller: invokeLater('$controllerProvider', 'register'), - - /** - * @ngdoc method - * @name angular.Module#directive - * @module ng - * @param {string|Object} name Directive name, or an object map of directives where the - * keys are the names and the values are the factories. - * @param {Function} directiveFactory Factory function for creating new instance of - * directives. - * @description - * See {@link ng.$compileProvider#directive $compileProvider.directive()}. - */ - directive: invokeLater('$compileProvider', 'directive'), - - /** - * @ngdoc method - * @name angular.Module#config - * @module ng - * @param {Function} configFn Execute this function on module load. Useful for service - * configuration. - * @description - * Use this method to register work which needs to be performed on module loading. - * For more about how to configure services, see - * {@link providers#providers_provider-recipe Provider Recipe}. - */ - config: config, - - /** - * @ngdoc method - * @name angular.Module#run - * @module ng - * @param {Function} initializationFn Execute this function after injector creation. - * Useful for application initialization. - * @description - * Use this method to register work which should be performed when the injector is done - * loading all modules. - */ - run: function(block) { - runBlocks.push(block); - return this; - } - }; - - if (configFn) { - config(configFn); - } - - return moduleInstance; - - /** - * @param {string} provider - * @param {string} method - * @param {String=} insertMethod - * @returns {angular.Module} - */ - function invokeLater(provider, method, insertMethod, queue) { - if (!queue) queue = invokeQueue; - return function() { - queue[insertMethod || 'push']([provider, method, arguments]); - return moduleInstance; - }; - } - }); - }; - }); - -} - -setupModuleLoader(window); -})(window); - -/** - * Closure compiler type information - * - * @typedef { { - * requires: !Array., - * invokeQueue: !Array.>, - * - * service: function(string, Function):angular.Module, - * factory: function(string, Function):angular.Module, - * value: function(string, *):angular.Module, - * - * filter: function(string, Function):angular.Module, - * - * init: function(Function):angular.Module - * } } - */ -angular.Module; - diff --git a/src/main/webapp/new/lib/angular/angular-loader.min.js b/src/main/webapp/new/lib/angular/angular-loader.min.js deleted file mode 100644 index 89032846..00000000 --- a/src/main/webapp/new/lib/angular/angular-loader.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(){'use strict';function d(a){return function(){var c=arguments[0],b,c="["+(a?a+":":"")+c+"] http://errors.angularjs.org/1.3.0-beta.17/"+(a?a+"/":"")+c;for(b=1;b - * - *
    - *
    You did not enter a field
    - *
    The value entered is too short
    - *
    - * - * ``` - * - * Now whatever key/value entries are present within the provided object (in this case `$error`) then - * the ngMessages directive will render the inner first ngMessage directive (depending if the key values - * match the attribute value present on each ngMessage directive). In other words, if your errors - * object contains the following data: - * - * ```javascript - * - * myField.$error = { minlength : true, required : false }; - * ``` - * - * Then the `required` message will be displayed first. When required is false then the `minlength` message - * will be displayed right after (since these messages are ordered this way in the template HTML code). - * The prioritization of each message is determined by what order they're present in the DOM. - * Therefore, instead of having custom JavaScript code determine the priority of what errors are - * present before others, the presentation of the errors are handled within the template. - * - * By default, ngMessages will only display one error at a time. However, if you wish to display all - * messages then the `ng-messages-multiple` attribute flag can be used on the element containing the - * ngMessages directive to make this happen. - * - * ```html - *
    ...
    - * ``` - * - * ## Reusing and Overriding Messages - * In addition to prioritization, ngMessages also allows for including messages from a remote or an inline - * template. This allows for generic collection of messages to be reused across multiple parts of an - * application. - * - * ```html - * - *
    - * ``` - * - * However, including generic messages may not be useful enough to match all input fields, therefore, - * `ngMessages` provides the ability to override messages defined in the remote template by redefining - * then within the directive container. - * - * ```html - * - * - * - *
    - * - * - *
    - * - *
    You did not enter your email address
    - * - * - *
    Your email address is invalid
    - *
    - *
    - * ``` - * - * In the example HTML code above the message that is set on required will override the corresponding - * required message defined within the remote template. Therefore, with particular input fields (such - * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied - * while more generic messages can be used to handle other, more general input errors. - * - * ## Animations - * If the `ngAnimate` module is active within the application then both the `ngMessages` and - * `ngMessage` directives will trigger animations whenever any messages are added and removed - * from the DOM by the `ngMessages` directive. - * - * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS - * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no - * animations present. Therefore, CSS transitions and keyframes as well as JavaScript animations can - * hook into the animations whenever these classes are added/removed. - * - * Let's say that our HTML code for our messages container looks like so: - * - * ```html - *
    - *
    ...
    - *
    ...
    - *
    - * ``` - * - * Then the CSS animation code for the message container looks like so: - * - * ```css - * .my-messages { - * transition:1s linear all; - * } - * .my-messages.ng-active { - * // messages are visible - * } - * .my-messages.ng-inactive { - * // messages are hidden - * } - * ``` - * - * Whenever an inner message is attached (becomes visible) or removed (becomes hidden) then the enter - * and leave animation is triggered for each particular element bound to the `ngMessage` directive. - * - * Therefore, the CSS code for the inner messages looks like so: - * - * ```css - * .some-message { - * transition:1s linear all; - * } - * - * .some-message.ng-enter {} - * .some-message.ng-enter.ng-enter-active {} - * - * .some-message.ng-leave {} - * .some-message.ng-leave.ng-leave-active {} - * ``` - * - * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. - */ -angular.module('ngMessages', []) - - /** - * @ngdoc directive - * @module ngMessages - * @name ngMessages - * @restrict AE - * - * @description - * `ngMessages` is a directive that is designed to show and hide messages based on the state - * of a key/value object that it listens on. The directive itself compliments error message - * reporting with the `ngModel` $error object (which stores a key/value state of validation errors). - * - * `ngMessages` manages the state of internal messages within its container element. The internal - * messages use the `ngMessage` directive and will be inserted/removed from the page depending - * on if they're present within the key/value object. By default, only one message will be displayed - * at a time and this depends on the prioritization of the messages within the template. (This can - * be changed by using the ng-messages-multiple on the directive container.) - * - * A remote template can also be used to promote message reuseability and messages can also be - * overridden. - * - * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. - * - * @usage - * ```html - * - * - * ... - * ... - * ... - * - * - * - * - * ... - * ... - * ... - * - * ``` - * - * @param {string} ngMessages an angular expression evaluating to a key/value object - * (this is typically the $error object on an ngModel instance). - * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true - * @param {string=} ngMessagesInclude|include when set, the specified template will be included into the ng-messages container - * - * @example - * - * - *
    - * - * - * - *
    myForm.myName.$error = {{ myForm.myName.$error | json }}
    - * - *
    - *
    You did not enter a field
    - *
    Your field is too short
    - *
    Your field is too long
    - *
    - *
    - *
    - * - * angular.module('ngMessagesExample', ['ngMessages']); - * - *
    - */ - .directive('ngMessages', ['$compile', '$animate', '$http', '$templateCache', - function($compile, $animate, $http, $templateCache) { - var ACTIVE_CLASS = 'ng-active'; - var INACTIVE_CLASS = 'ng-inactive'; - - return { - restrict: 'AE', - controller: ['$scope', function($scope) { - this.$renderNgMessageClasses = angular.noop; - - var messages = []; - this.registerMessage = function(index, message) { - for(var i = 0; i < messages.length; i++) { - if(messages[i].type == message.type) { - if(index != i) { - var temp = messages[index]; - messages[index] = messages[i]; - if(index < messages.length) { - messages[i] = temp; - } else { - messages.splice(0, i); //remove the old one (and shift left) - } - } - return; - } - } - messages.splice(index, 0, message); //add the new one (and shift right) - }; - - this.renderMessages = function(values, multiple) { - values = values || {}; - - var found; - angular.forEach(messages, function(message) { - if((!found || multiple) && truthyVal(values[message.type])) { - message.attach(); - found = true; - } else { - message.detach(); - } - }); - - this.renderElementClasses(found); - - function truthyVal(value) { - return value !== null && value !== false && value; - } - }; - }], - require: 'ngMessages', - link: function($scope, element, $attrs, ctrl) { - ctrl.renderElementClasses = function(bool) { - bool ? $animate.setClass(element, ACTIVE_CLASS, INACTIVE_CLASS) - : $animate.setClass(element, INACTIVE_CLASS, ACTIVE_CLASS); - }; - - //JavaScript treats empty strings as false, but ng-message-multiple by itself is an empty string - var multiple = angular.isString($attrs.ngMessagesMultiple) || - angular.isString($attrs.multiple); - - var cachedValues, watchAttr = $attrs.ngMessages || $attrs['for']; //for is a reserved keyword - $scope.$watchCollection(watchAttr, function(values) { - cachedValues = values; - ctrl.renderMessages(values, multiple); - }); - - var tpl = $attrs.ngMessagesInclude || $attrs.include; - if(tpl) { - $http.get(tpl, { cache: $templateCache }) - .success(function processTemplate(html) { - var after, container = angular.element('
    ').html(html); - angular.forEach(container.children(), function(elm) { - elm = angular.element(elm); - after ? after.after(elm) - : element.prepend(elm); //start of the container - after = elm; - $compile(elm)($scope); - }); - ctrl.renderMessages(cachedValues, multiple); - }); - } - } - }; - }]) - - - /** - * @ngdoc directive - * @name ngMessage - * @restrict AE - * @scope - * - * @description - * `ngMessage` is a directive with the purpose to show and hide a particular message. - * For `ngMessage` to operate, a parent `ngMessages` directive on a parent DOM element - * must be situated since it determines which messages are visible based on the state - * of the provided key/value map that `ngMessages` listens on. - * - * @usage - * ```html - * - * - * ... - * ... - * ... - * - * - * - * - * ... - * ... - * ... - * - * ``` - * - * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. - * - * @param {string} ngMessage a string value corresponding to the message key. - */ - .directive('ngMessage', ['$animate', function($animate) { - var COMMENT_NODE = 8; - return { - require: '^ngMessages', - transclude: 'element', - terminal: true, - restrict: 'AE', - link: function($scope, $element, $attrs, ngMessages, $transclude) { - var index, element; - - var commentNode = $element[0]; - var parentNode = commentNode.parentNode; - for(var i = 0, j = 0; i < parentNode.childNodes.length; i++) { - var node = parentNode.childNodes[i]; - if(node.nodeType == COMMENT_NODE && node.nodeValue.indexOf('ngMessage') >= 0) { - if(node === commentNode) { - index = j; - break; - } - j++; - } - } - - ngMessages.registerMessage(index, { - type : $attrs.ngMessage || $attrs.when, - attach : function() { - if(!element) { - $transclude($scope, function(clone) { - $animate.enter(clone, null, $element); - element = clone; - }); - } - }, - detach : function(now) { - if(element) { - $animate.leave(element); - element = null; - } - } - }); - } - }; - }]); - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-messages.min.js b/src/main/webapp/new/lib/angular/angular-messages.min.js deleted file mode 100644 index f6c5385d..00000000 --- a/src/main/webapp/new/lib/angular/angular-messages.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(r,e,s){'use strict';e.module("ngMessages",[]).directive("ngMessages",["$compile","$animate","$http","$templateCache",function(q,k,l,m){return{restrict:"AE",controller:["$scope",function(h){this.$renderNgMessageClasses=e.noop;var b=[];this.registerMessage=function(a,d){for(var c=0;c").html(a); -e.forEach(a.children(),function(a){a=e.element(a);g?g.after(a):b.prepend(a);g=a;q(a)(h)});d.renderMessages(f,c)})}}}]).directive("ngMessage",["$animate",function(e){return{require:"^ngMessages",transclude:"element",terminal:!0,restrict:"AE",link:function(k,l,m,h,b){for(var a,d,c=l[0],f=c.parentNode,n=0,g=0;n 0 && iteration >= count) { - var fnIndex; - deferred.resolve(iteration); - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - repeatFns.splice(fnIndex, 1); - } - } - - if (!skipApply) $rootScope.$apply(); - } - - repeatFns.push({ - nextTime:(now + delay), - delay: delay, - fn: tick, - id: nextRepeatId, - deferred: deferred - }); - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); - - nextRepeatId++; - return promise; - }; - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {promise} promise A promise from calling the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully cancelled. - */ - $interval.cancel = function(promise) { - if(!promise) return false; - var fnIndex; - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - repeatFns[fnIndex].deferred.reject('canceled'); - repeatFns.splice(fnIndex, 1); - return true; - } - - return false; - }; - - /** - * @ngdoc method - * @name $interval#flush - * @description - * - * Runs interval tasks scheduled to be run in the next `millis` milliseconds. - * - * @param {number=} millis maximum timeout amount to flush up until. - * - * @return {number} The amount of time moved forward. - */ - $interval.flush = function(millis) { - now += millis; - while (repeatFns.length && repeatFns[0].nextTime <= now) { - var task = repeatFns[0]; - task.fn(); - task.nextTime += task.delay; - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); - } - return millis; - }; - - return $interval; - }]; -}; - - -/* jshint -W101 */ -/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! - * This directive should go inside the anonymous function but a bug in JSHint means that it would - * not be enacted early enough to prevent the warning. - */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; - -function jsonStringToDate(string) { - var match; - if (match = string.match(R_ISO8061_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0; - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); - } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, - int(match[5]||0) - tzMin, - int(match[6]||0), - int(match[7]||0)); - return date; - } - return string; -} - -function int(str) { - return parseInt(str, 10); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; -} - - -/** - * @ngdoc type - * @name angular.mock.TzDate - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. - * - * Mock of the Date type which has its timezone specified via constructor arg. - * - * The main purpose is to create Date-like instances with timezone fixed to the specified timezone - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - * - * ```js - * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); - * newYearInBratislava.getTimezoneOffset() => -60; - * newYearInBratislava.getFullYear() => 2010; - * newYearInBratislava.getMonth() => 0; - * newYearInBratislava.getDate() => 1; - * newYearInBratislava.getHours() => 0; - * newYearInBratislava.getMinutes() => 0; - * newYearInBratislava.getSeconds() => 0; - * ``` - * - */ -angular.mock.TzDate = function (offset, timestamp) { - var self = new Date(0); - if (angular.isString(timestamp)) { - var tsStr = timestamp; - - self.origDate = jsonStringToDate(timestamp); - - timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) - throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" - }; - } else { - self.origDate = new Date(timestamp); - } - - var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; - self.date = new Date(timestamp + self.offsetDiff); - - self.getTime = function() { - return self.date.getTime() - self.offsetDiff; - }; - - self.toLocaleDateString = function() { - return self.date.toLocaleDateString(); - }; - - self.getFullYear = function() { - return self.date.getFullYear(); - }; - - self.getMonth = function() { - return self.date.getMonth(); - }; - - self.getDate = function() { - return self.date.getDate(); - }; - - self.getHours = function() { - return self.date.getHours(); - }; - - self.getMinutes = function() { - return self.date.getMinutes(); - }; - - self.getSeconds = function() { - return self.date.getSeconds(); - }; - - self.getMilliseconds = function() { - return self.date.getMilliseconds(); - }; - - self.getTimezoneOffset = function() { - return offset * 60; - }; - - self.getUTCFullYear = function() { - return self.origDate.getUTCFullYear(); - }; - - self.getUTCMonth = function() { - return self.origDate.getUTCMonth(); - }; - - self.getUTCDate = function() { - return self.origDate.getUTCDate(); - }; - - self.getUTCHours = function() { - return self.origDate.getUTCHours(); - }; - - self.getUTCMinutes = function() { - return self.origDate.getUTCMinutes(); - }; - - self.getUTCSeconds = function() { - return self.origDate.getUTCSeconds(); - }; - - self.getUTCMilliseconds = function() { - return self.origDate.getUTCMilliseconds(); - }; - - self.getDay = function() { - return self.date.getDay(); - }; - - // provide this method only on browsers that already have it - if (self.toISOString) { - self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; - }; - } - - //hide all methods not implemented in this mock that the Date prototype exposes - var unimplementedMethods = ['getUTCDay', - 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', - 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', - 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', - 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', - 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - - angular.forEach(unimplementedMethods, function(methodName) { - self[methodName] = function() { - throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); - }; - }); - - return self; -}; - -//make "tzDateInstance instanceof Date" return true -angular.mock.TzDate.prototype = Date.prototype; -/* jshint +W101 */ - -angular.mock.animate = angular.module('ngAnimateMock', ['ng']) - - .config(['$provide', function($provide) { - - var reflowQueue = []; - $provide.value('$$animateReflow', function(fn) { - var index = reflowQueue.length; - reflowQueue.push(fn); - return function cancel() { - reflowQueue.splice(index, 1); - }; - }); - - $provide.decorator('$animate', ['$delegate', '$$asyncCallback', - function($delegate, $$asyncCallback) { - var animate = { - queue : [], - enabled : $delegate.enabled, - triggerCallbacks : function() { - $$asyncCallback.flush(); - }, - triggerReflow : function() { - angular.forEach(reflowQueue, function(fn) { - fn(); - }); - reflowQueue = []; - } - }; - - angular.forEach( - ['enter','leave','move','addClass','removeClass','setClass'], function(method) { - animate[method] = function() { - animate.queue.push({ - event : method, - element : arguments[0], - args : arguments - }); - return $delegate[method].apply($delegate, arguments); - }; - }); - - return animate; - }]); - - }]); - - -/** - * @ngdoc function - * @name angular.mock.dump - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available function. - * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for - * debugging. - * - * This method is also available on window, where it can be used to display objects on debug - * console. - * - * @param {*} object - any object to turn into string. - * @return {string} a serialized string of the argument - */ -angular.mock.dump = function(object) { - return serialize(object); - - function serialize(object) { - var out; - - if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
    '); - angular.forEach(object, function(element) { - out.append(angular.element(element).clone()); - }); - out = out.html(); - } else if (angular.isArray(object)) { - out = []; - angular.forEach(object, function(o) { - out.push(serialize(o)); - }); - out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { - if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { - out = serializeScope(object); - } else if (object instanceof Error) { - out = object.stack || ('' + object.name + ': ' + object.message); - } else { - // TODO(i): this prevents methods being logged, - // we should have a better way to serialize objects - out = angular.toJson(object, true); - } - } else { - out = String(object); - } - - return out; - } - - function serializeScope(scope, offset) { - offset = offset || ' '; - var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { - if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { - log.push(' ' + key + ': ' + angular.toJson(scope[key])); - } - } - var child = scope.$$childHead; - while(child) { - log.push(serializeScope(child, offset + ' ')); - child = child.$$nextSibling; - } - log.push('}'); - return log.join('\n' + offset); - } -}; - -/** - * @ngdoc service - * @name $httpBackend - * @description - * Fake HTTP backend implementation suitable for unit testing applications that use the - * {@link ng.$http $http service}. - * - * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less - * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. - * - * During unit testing, we want our unit tests to run quickly and have no external dependencies so - * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or - * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is - * to verify whether a certain request has been sent or not, or alternatively just let the - * application make requests, respond with pre-trained responses and assert that the end result is - * what we expect it to be. - * - * This mock implementation can be used to respond with static or dynamic responses via the - * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). - * - * When an Angular application needs some data from a server, it calls the $http service, which - * sends the request to a real server using $httpBackend service. With dependency injection, it is - * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to a real server. - * - * There are two ways to specify what test data should be returned as http responses by the mock - * backend when the code under test makes http requests: - * - * - `$httpBackend.expect` - specifies a request expectation - * - `$httpBackend.when` - specifies a backend definition - * - * - * # Request Expectations vs Backend Definitions - * - * Request expectations provide a way to make assertions about requests made by the application and - * to define responses for those requests. The test will fail if the expected requests are not made - * or they are made in the wrong order. - * - * Backend definitions allow you to define a fake backend for your application which doesn't assert - * if a particular request was made or not, it just returns a trained response if a request is made. - * The test will pass whether or not the request gets made during testing. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Request expectationsBackend definitions
    Syntax.expect(...).respond(...).when(...).respond(...)
    Typical usagestrict unit testsloose (black-box) unit testing
    Fulfills multiple requestsNOYES
    Order of requests mattersYESNO
    Request requiredYESNO
    Response requiredoptional (see below)YES
    - * - * In cases where both backend definitions and request expectations are specified during unit - * testing, the request expectations are evaluated first. - * - * If a request expectation has no response specified, the algorithm will search your backend - * definitions for an appropriate response. - * - * If a request didn't match any expectation or if the expectation doesn't have the response - * defined, the backend definitions are evaluated in sequential order to see if any of them match - * the request. The response from the first matched definition is returned. - * - * - * # Flushing HTTP requests - * - * The $httpBackend used in production always responds to requests asynchronously. If we preserved - * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, - * to follow and to maintain. But neither can the testing mock respond synchronously; that would - * change the execution of the code under test. For this reason, the mock $httpBackend has a - * `flush()` method, which allows the test to explicitly flush pending requests. This preserves - * the async api of the backend, while allowing the test to execute synchronously. - * - * - * # Unit testing with mock $httpBackend - * The following code shows how to setup and use the mock backend when unit testing a controller. - * First we create the controller under test: - * - ```js - // The controller code - function MyController($scope, $http) { - var authToken; - - $http.get('/auth.py').success(function(data, status, headers) { - authToken = headers('A-Token'); - $scope.user = data; - }); - - $scope.saveMessage = function(message) { - var headers = { 'Authorization': authToken }; - $scope.status = 'Saving...'; - - $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { - $scope.status = ''; - }).error(function() { - $scope.status = 'ERROR!'; - }); - }; - } - ``` - * - * Now we setup the mock backend and create the test specs: - * - ```js - // testing controller - describe('MyController', function() { - var $httpBackend, $rootScope, createController; - - beforeEach(inject(function($injector) { - // Set up the mock http service responses - $httpBackend = $injector.get('$httpBackend'); - // backend definition common for all tests - $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'}); - - // Get hold of a scope (i.e. the root scope) - $rootScope = $injector.get('$rootScope'); - // The $controller service is used to create instances of controllers - var $controller = $injector.get('$controller'); - - createController = function() { - return $controller('MyController', {'$scope' : $rootScope }); - }; - })); - - - afterEach(function() { - $httpBackend.verifyNoOutstandingExpectation(); - $httpBackend.verifyNoOutstandingRequest(); - }); - - - it('should fetch authentication token', function() { - $httpBackend.expectGET('/auth.py'); - var controller = createController(); - $httpBackend.flush(); - }); - - - it('should send msg to server', function() { - var controller = createController(); - $httpBackend.flush(); - - // now you don’t care about the authentication, but - // the controller will still send the request and - // $httpBackend will respond without you having to - // specify the expectation and response for this request - - $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); - $rootScope.saveMessage('message content'); - expect($rootScope.status).toBe('Saving...'); - $httpBackend.flush(); - expect($rootScope.status).toBe(''); - }); - - - it('should send auth header', function() { - var controller = createController(); - $httpBackend.flush(); - - $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { - // check if the header was send, if it wasn't the expectation won't - // match the request and the test will fail - return headers['Authorization'] == 'xxx'; - }).respond(201, ''); - - $rootScope.saveMessage('whatever'); - $httpBackend.flush(); - }); - }); - ``` - */ -angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', createHttpBackendMock]; -}; - -/** - * General factory function for $httpBackend mock. - * Returns instance for unit testing (when no arguments specified): - * - passing through is disabled - * - auto flushing is disabled - * - * Returns instance for e2e testing (when `$delegate` and `$browser` specified): - * - passing through (delegating request to real backend) is enabled - * - auto flushing is enabled - * - * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) - * @param {Object=} $browser Auto-flushing enabled if specified - * @return {Object} Instance of $httpBackend mock - */ -function createHttpBackendMock($rootScope, $delegate, $browser) { - var definitions = [], - expectations = [], - responses = [], - responsesPush = angular.bind(responses, responses.push), - copy = angular.copy; - - function createResponse(status, data, headers, statusText) { - if (angular.isFunction(status)) return status; - - return function() { - return angular.isNumber(status) - ? [status, data, headers, statusText] - : [200, status, data]; - }; - } - - // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { - var xhr = new MockXhr(), - expectation = expectations[0], - wasExpected = false; - - function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) - ? data - : angular.toJson(data); - } - - function wrapResponse(wrapped) { - if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); - - return handleResponse; - - function handleResponse() { - var response = wrapped.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), - copy(response[3] || '')); - } - - function handleTimeout() { - for (var i = 0, ii = responses.length; i < ii; i++) { - if (responses[i] === handleResponse) { - responses.splice(i, 1); - callback(-1, undefined, ''); - break; - } - } - } - } - - if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) - throw new Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); - - if (!expectation.matchHeaders(headers)) - throw new Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); - - expectations.shift(); - - if (expectation.response) { - responses.push(wrapResponse(expectation)); - return; - } - wasExpected = true; - } - - var i = -1, definition; - while ((definition = definitions[++i])) { - if (definition.match(method, url, data, headers || {})) { - if (definition.response) { - // if $browser specified, we do auto flush all requests - ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); - } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); - } else throw new Error('No response defined !'); - return; - } - } - throw wasExpected ? - new Error('No response defined !') : - new Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); - } - - /** - * @ngdoc method - * @name $httpBackend#when - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). - */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), - chain = { - respond: function(status, data, headers, statusText) { - definition.response = createResponse(status, data, headers, statusText); - } - }; - - if ($browser) { - chain.passThrough = function() { - definition.passThrough = true; - }; - } - - definitions.push(definition); - return chain; - }; - - /** - * @ngdoc method - * @name $httpBackend#whenGET - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenHEAD - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenDELETE - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenPOST - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenPUT - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenJSONP - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('when'); - - - /** - * @ngdoc method - * @name $httpBackend#expect - * @description - * Creates a new request expectation. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). - */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers); - expectations.push(expectation); - return { - respond: function (status, data, headers, statusText) { - expectation.response = createResponse(status, data, headers, statusText); - } - }; - }; - - - /** - * @ngdoc method - * @name $httpBackend#expectGET - * @description - * Creates a new request expectation for GET requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. See #expect for more info. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectHEAD - * @description - * Creates a new request expectation for HEAD requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectDELETE - * @description - * Creates a new request expectation for DELETE requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPOST - * @description - * Creates a new request expectation for POST requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPUT - * @description - * Creates a new request expectation for PUT requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPATCH - * @description - * Creates a new request expectation for PATCH requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectJSONP - * @description - * Creates a new request expectation for JSONP requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - createShortMethods('expect'); - - - /** - * @ngdoc method - * @name $httpBackend#flush - * @description - * Flushes all pending requests using the trained responses. - * - * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, - * all pending requests will be flushed. If there are no pending requests when the flush method - * is called an exception is thrown (as this typically a sign of programming error). - */ - $httpBackend.flush = function(count) { - $rootScope.$digest(); - if (!responses.length) throw new Error('No pending request to flush !'); - - if (angular.isDefined(count)) { - while (count--) { - if (!responses.length) throw new Error('No more pending request to flush !'); - responses.shift()(); - } - } else { - while (responses.length) { - responses.shift()(); - } - } - $httpBackend.verifyNoOutstandingExpectation(); - }; - - - /** - * @ngdoc method - * @name $httpBackend#verifyNoOutstandingExpectation - * @description - * Verifies that all of the requests defined via the `expect` api were made. If any of the - * requests were not made, verifyNoOutstandingExpectation throws an exception. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - * ```js - * afterEach($httpBackend.verifyNoOutstandingExpectation); - * ``` - */ - $httpBackend.verifyNoOutstandingExpectation = function() { - $rootScope.$digest(); - if (expectations.length) { - throw new Error('Unsatisfied requests: ' + expectations.join(', ')); - } - }; - - - /** - * @ngdoc method - * @name $httpBackend#verifyNoOutstandingRequest - * @description - * Verifies that there are no outstanding requests that need to be flushed. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - * ```js - * afterEach($httpBackend.verifyNoOutstandingRequest); - * ``` - */ - $httpBackend.verifyNoOutstandingRequest = function() { - if (responses.length) { - throw new Error('Unflushed requests: ' + responses.length); - } - }; - - - /** - * @ngdoc method - * @name $httpBackend#resetExpectations - * @description - * Resets all request expectations, but preserves all backend definitions. Typically, you would - * call resetExpectations during a multiple-phase test when you want to reuse the same instance of - * $httpBackend mock. - */ - $httpBackend.resetExpectations = function() { - expectations.length = 0; - responses.length = 0; - }; - - return $httpBackend; - - - function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers); - }; - }); - - angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers); - }; - }); - } -} - -function MockHttpExpectation(method, url, data, headers) { - - this.data = data; - this.headers = headers; - - this.match = function(m, u, d, h) { - if (method != m) return false; - if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; - return true; - }; - - this.matchUrl = function(u) { - if (!url) return true; - if (angular.isFunction(url.test)) return url.test(u); - if (angular.isFunction(url)) return url(u); - return url == u; - }; - - this.matchHeaders = function(h) { - if (angular.isUndefined(headers)) return true; - if (angular.isFunction(headers)) return headers(h); - return angular.equals(headers, h); - }; - - this.matchData = function(d) { - if (angular.isUndefined(data)) return true; - if (data && angular.isFunction(data.test)) return data.test(d); - if (data && angular.isFunction(data)) return data(d); - if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); - return data == d; - }; - - this.toString = function() { - return method + ' ' + url; - }; -} - -function createMockXhr() { - return new MockXhr(); -} - -function MockXhr() { - - // hack for testing $http, $httpBackend - MockXhr.$$lastInstance = this; - - this.open = function(method, url, async) { - this.$$method = method; - this.$$url = url; - this.$$async = async; - this.$$reqHeaders = {}; - this.$$respHeaders = {}; - }; - - this.send = function(data) { - this.$$data = data; - }; - - this.setRequestHeader = function(key, value) { - this.$$reqHeaders[key] = value; - }; - - this.getResponseHeader = function(name) { - // the lookup must be case insensitive, - // that's why we try two quick lookups first and full scan last - var header = this.$$respHeaders[name]; - if (header) return header; - - name = angular.lowercase(name); - header = this.$$respHeaders[name]; - if (header) return header; - - header = undefined; - angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) == name) header = headerVal; - }); - return header; - }; - - this.getAllResponseHeaders = function() { - var lines = []; - - angular.forEach(this.$$respHeaders, function(value, key) { - lines.push(key + ': ' + value); - }); - return lines.join('\n'); - }; - - this.abort = angular.noop; -} - - -/** - * @ngdoc service - * @name $timeout - * @description - * - * This service is just a simple decorator for {@link ng.$timeout $timeout} service - * that adds a "flush" and "verifyNoPendingTasks" methods. - */ - -angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function ($delegate, $browser) { - - /** - * @ngdoc method - * @name $timeout#flush - * @description - * - * Flushes the queue of pending tasks. - * - * @param {number=} delay maximum timeout amount to flush up until - */ - $delegate.flush = function(delay) { - $browser.defer.flush(delay); - }; - - /** - * @ngdoc method - * @name $timeout#verifyNoPendingTasks - * @description - * - * Verifies that there are no pending tasks that need to be flushed. - */ - $delegate.verifyNoPendingTasks = function() { - if ($browser.deferredFns.length) { - throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + - formatPendingTasksAsString($browser.deferredFns)); - } - }; - - function formatPendingTasksAsString(tasks) { - var result = []; - angular.forEach(tasks, function(task) { - result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); - }); - - return result.join(', '); - } - - return $delegate; -}]; - -angular.mock.$RAFDecorator = ['$delegate', function($delegate) { - var queue = []; - var rafFn = function(fn) { - var index = queue.length; - queue.push(fn); - return function() { - queue.splice(index, 1); - }; - }; - - rafFn.supported = $delegate.supported; - - rafFn.flush = function() { - if(queue.length === 0) { - throw new Error('No rAF callbacks present'); - } - - var length = queue.length; - for(var i=0;i
    '); - }; -}; - -/** - * @ngdoc module - * @name ngMock - * @packageName angular-mocks - * @description - * - * # ngMock - * - * The `ngMock` module provides support to inject and mock Angular services into unit tests. - * In addition, ngMock also extends various core ng services such that they can be - * inspected and controlled in a synchronous manner within test code. - * - * - *
    - * - */ -angular.module('ngMock', ['ng']).provider({ - $browser: angular.mock.$BrowserProvider, - $exceptionHandler: angular.mock.$ExceptionHandlerProvider, - $log: angular.mock.$LogProvider, - $interval: angular.mock.$IntervalProvider, - $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider -}).config(['$provide', function($provide) { - $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); - $provide.decorator('$$rAF', angular.mock.$RAFDecorator); - $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); -}]); - -/** - * @ngdoc module - * @name ngMockE2E - * @module ngMockE2E - * @packageName angular-mocks - * @description - * - * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. - * Currently there is only one mock present in this module - - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. - */ -angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { - $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}]); - -/** - * @ngdoc service - * @name $httpBackend - * @module ngMockE2E - * @description - * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of - * applications that use the {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for unit testing please see - * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. - * - * This implementation can be used to respond with static or dynamic responses via the `when` api - * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the - * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch - * templates from a webserver). - * - * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application - * is being developed with the real backend api replaced with a mock, it is often desirable for - * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch - * templates or static files from the webserver). To configure the backend with this behavior - * use the `passThrough` request handler of `when` instead of `respond`. - * - * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests - * automatically, closely simulating the behavior of the XMLHttpRequest object. - * - * To setup the application to run with this http backend, you have to create a module that depends - * on the `ngMockE2E` and your application modules and defines the fake backend: - * - * ```js - * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); - * myAppDev.run(function($httpBackend) { - * phones = [{name: 'phone1'}, {name: 'phone2'}]; - * - * // returns the current list of phones - * $httpBackend.whenGET('/phones').respond(phones); - * - * // adds a new phone to the phones array - * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { - * var phone = angular.fromJson(data); - * phones.push(phone); - * return [200, phone, {}]; - * }); - * $httpBackend.whenGET(/^\/templates\//).passThrough(); - * //... - * }); - * ``` - * - * Afterwards, bootstrap your app with this new module. - */ - -/** - * @ngdoc method - * @name $httpBackend#when - * @module ngMockE2E - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string), response headers - * (Object), and the text for the status (string). - * - passThrough – `{function()}` – Any request matching a backend definition with - * `passThrough` handler will be passed through to the real backend (an XHR request will be made - * to the server.) - */ - -/** - * @ngdoc method - * @name $httpBackend#whenGET - * @module ngMockE2E - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenHEAD - * @module ngMockE2E - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenDELETE - * @module ngMockE2E - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPOST - * @module ngMockE2E - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPUT - * @module ngMockE2E - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPATCH - * @module ngMockE2E - * @description - * Creates a new backend definition for PATCH requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenJSONP - * @module ngMockE2E - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. - */ -angular.mock.e2e = {}; -angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; - - -angular.mock.clearDataCache = function() { - var key, - cache = angular.element.cache; - - for(key in cache) { - if (Object.prototype.hasOwnProperty.call(cache,key)) { - var handle = cache[key].handle; - - handle && angular.element(handle.elem).off(); - delete cache[key]; - } - } -}; - - -if(window.jasmine || window.mocha) { - - var currentSpec = null, - isSpecRunning = function() { - return !!currentSpec; - }; - - - (window.beforeEach || window.setup)(function() { - currentSpec = this; - }); - - (window.afterEach || window.teardown)(function() { - var injector = currentSpec.$injector; - - angular.forEach(currentSpec.$modules, function(module) { - if (module && module.$$hashKey) { - module.$$hashKey = undefined; - } - }); - - currentSpec.$injector = null; - currentSpec.$modules = null; - currentSpec = null; - - if (injector) { - injector.get('$rootElement').off(); - injector.get('$browser').pollFns.length = 0; - } - - angular.mock.clearDataCache(); - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - - /** - * @ngdoc function - * @name angular.mock.module - * @description - * - * *NOTE*: This function is also published on window for easy access.
    - * - * This function registers a module configuration code. It collects the configuration information - * which will be used when the injector is created by {@link angular.mock.inject inject}. - * - * See {@link angular.mock.inject inject} for usage example - * - * @param {...(string|Function|Object)} fns any number of modules which are represented as string - * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an - * object literal is passed they will be registered as values in the module, the key being - * the module name and the value being what is returned. - */ - window.module = angular.mock.module = function() { - var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - if (currentSpec.$injector) { - throw new Error('Injector already created, can not register a module!'); - } else { - var modules = currentSpec.$modules || (currentSpec.$modules = []); - angular.forEach(moduleFns, function(module) { - if (angular.isObject(module) && !angular.isArray(module)) { - modules.push(function($provide) { - angular.forEach(module, function(value, key) { - $provide.value(key, value); - }); - }); - } else { - modules.push(module); - } - }); - } - } - }; - - /** - * @ngdoc function - * @name angular.mock.inject - * @description - * - * *NOTE*: This function is also published on window for easy access.
    - * - * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link auto.$injector $injector} per test, which is then used for - * resolving references. - * - * - * ## Resolving References (Underscore Wrapping) - * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this - * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable - * that is declared in the scope of the `describe()` block. Since we would, most likely, want - * the variable to have the same name of the reference we have a problem, since the parameter - * to the `inject()` function would hide the outer variable. - * - * To help with this, the injected parameters can, optionally, be enclosed with underscores. - * These are ignored by the injector when the reference name is resolved. - * - * For example, the parameter `_myService_` would be resolved as the reference `myService`. - * Since it is available in the function body as _myService_, we can then assign it to a variable - * defined in an outer scope. - * - * ``` - * // Defined out reference variable outside - * var myService; - * - * // Wrap the parameter in underscores - * beforeEach( inject( function(_myService_){ - * myService = _myService_; - * })); - * - * // Use myService in a series of tests. - * it('makes use of myService', function() { - * myService.doStuff(); - * }); - * - * ``` - * - * See also {@link angular.mock.module angular.mock.module} - * - * ## Example - * Example of what a typical jasmine tests looks like with the inject method. - * ```js - * - * angular.module('myApplicationModule', []) - * .value('mode', 'app') - * .value('version', 'v1.0.1'); - * - * - * describe('MyApp', function() { - * - * // You need to load modules that you want to test, - * // it loads only the "ng" module by default. - * beforeEach(module('myApplicationModule')); - * - * - * // inject() is used to inject arguments of all given functions - * it('should provide a version', inject(function(mode, version) { - * expect(version).toEqual('v1.0.1'); - * expect(mode).toEqual('app'); - * })); - * - * - * // The inject and module method can also be used inside of the it or beforeEach - * it('should override a version and test the new version is injected', function() { - * // module() takes functions or strings (module aliases) - * module(function($provide) { - * $provide.value('version', 'overridden'); // override version here - * }); - * - * inject(function(version) { - * expect(version).toEqual('overridden'); - * }); - * }); - * }); - * - * ``` - * - * @param {...Function} fns any number of functions which will be injected using the injector. - */ - - - - var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { - this.message = e.message; - this.name = e.name; - if (e.line) this.line = e.line; - if (e.sourceId) this.sourceId = e.sourceId; - if (e.stack && errorForStack) - this.stack = e.stack + '\n' + errorForStack.stack; - if (e.stackArray) this.stackArray = e.stackArray; - }; - ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; - - window.inject = angular.mock.inject = function() { - var blockFns = Array.prototype.slice.call(arguments, 0); - var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn.call(currentSpec) : workFn; - ///////////////////// - function workFn() { - var modules = currentSpec.$modules || []; - var strictDi = !!currentSpec.$injectorStrict; - modules.unshift('ngMock'); - modules.unshift('ng'); - var injector = currentSpec.$injector; - if (!injector) { - if (strictDi) { - // If strictDi is enabled, annotate the providerInjector blocks - angular.forEach(modules, function(moduleFn) { - if (typeof moduleFn === "function") { - angular.injector.$$annotate(moduleFn); - } - }); - } - injector = currentSpec.$injector = angular.injector(modules, strictDi); - currentSpec.$injectorStrict = strictDi; - } - for(var i = 0, ii = blockFns.length; i < ii; i++) { - if (currentSpec.$injectorStrict) { - // If the injector is strict / strictDi, and the spec wants to inject using automatic - // annotation, then annotate the function here. - injector.annotate(blockFns[i]); - } - try { - /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ - injector.invoke(blockFns[i] || angular.noop, this); - /* jshint +W040 */ - } catch (e) { - if (e.stack && errorForStack) { - throw new ErrorAddingDeclarationLocationStack(e, errorForStack); - } - throw e; - } finally { - errorForStack = null; - } - } - } - }; - - - angular.mock.inject.strictDi = function(value) { - value = arguments.length ? !!value : true; - return isSpecRunning() ? workFn() : workFn; - - function workFn() { - if (value !== currentSpec.$injectorStrict) { - if (currentSpec.$injector) { - throw new Error('Injector already created, can not modify strict annotations'); - } else { - currentSpec.$injectorStrict = value; - } - } - } - }; -} - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-resource.js b/src/main/webapp/new/lib/angular/angular-resource.js deleted file mode 100644 index 40d4634f..00000000 --- a/src/main/webapp/new/lib/angular/angular-resource.js +++ /dev/null @@ -1,660 +0,0 @@ -/** - * @license AngularJS v1.3.0-beta.17 - * (c) 2010-2014 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) {'use strict'; - -var $resourceMinErr = angular.$$minErr('$resource'); - -// Helper functions and regex to lookup a dotted path on an object -// stopping at undefined/null. The path must be composed of ASCII -// identifiers (just like $parse) -var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; - -function isValidDottedPath(path) { - return (path != null && path !== '' && path !== 'hasOwnProperty' && - MEMBER_NAME_REGEX.test('.' + path)); -} - -function lookupDottedPath(obj, path) { - if (!isValidDottedPath(path)) { - throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); - } - var keys = path.split('.'); - for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { - var key = keys[i]; - obj = (obj !== null) ? obj[key] : undefined; - } - return obj; -} - -/** - * Create a shallow copy of an object and clear other fields from the destination - */ -function shallowClearAndCopy(src, dst) { - dst = dst || {}; - - angular.forEach(dst, function(value, key){ - delete dst[key]; - }); - - for (var key in src) { - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } - } - - return dst; -} - -/** - * @ngdoc module - * @name ngResource - * @description - * - * # ngResource - * - * The `ngResource` module provides interaction support with RESTful services - * via the $resource service. - * - * - *
    - * - * See {@link ngResource.$resource `$resource`} for usage. - */ - -/** - * @ngdoc service - * @name $resource - * @requires $http - * - * @description - * A factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link ng.$http $http} service. - * - * Requires the {@link ngResource `ngResource`} module to be installed. - * - * By default, trailing slashes will be stripped from the calculated URLs, - * which can pose problems with server backends that do not expect that - * behavior. This can be disabled by configuring the `$resourceProvider` like - * this: - * - * ```js - app.config(['$resourceProvider', function ($resourceProvider) { - // Don't strip trailing slashes from calculated URLs - $resourceProvider.defaults.stripTrailingSlashes = false; - }]); - * ``` - * - * @param {string} url A parametrized URL template with parameters prefixed by `:` as in - * `/user/:username`. If you are using a URL with a port number (e.g. - * `http://example.com:8080/api`), it will be respected. - * - * If you are using a url with a suffix, just add the suffix, like this: - * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` - * or even `$resource('http://example.com/resource/:resource_id.:format')` - * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be - * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you - * can escape it with `/\.`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. If any of the parameter value is a function, it will be executed every time - * when a param value needs to be obtained for a request (unless the param was overridden). - * - * Each key value in the parameter object is first bound to url template if present and then any - * excess keys are appended to the url search query after the `?`. - * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - * URL `/path/greet?salutation=Hello`. - * - * If the parameter value is prefixed with `@` then the value of that parameter will be taken - * from the corresponding key on the data object (useful for non-GET operations). - * - * @param {Object.=} actions Hash with declaration of custom action that should extend - * the default set of resource actions. The declaration should be created in the format of {@link - * ng.$http#usage_parameters $http.config}: - * - * {action1: {method:?, params:?, isArray:?, headers:?, ...}, - * action2: {method:?, params:?, isArray:?, headers:?, ...}, - * ...} - * - * Where: - * - * - **`action`** – {string} – The name of action. This name becomes the name of the method on - * your resource object. - * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, - * `DELETE`, `JSONP`, etc). - * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of - * the parameter value is a function, it will be executed every time when a param value needs to - * be obtained for a request (unless the param was overridden). - * - **`url`** – {string} – action specific `url` override. The url templating is supported just - * like for the resource-level urls. - * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, - * see `returns` section. - * - **`transformRequest`** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * request body and headers and returns its transformed (typically serialized) version. - * - **`transformResponse`** – - * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. - * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that - * should abort the request when resolved. - * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See - * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) - * for more information. - * - **`responseType`** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. - * - * @param {Object} options Hash with custom settings that should extend the - * default `$resourceProvider` behavior. The only supported option is - * - * Where: - * - * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing - * slashes from any calculated URL will be stripped. (Defaults to true.) - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - * optionally extended with custom `actions`. The default set contains these actions: - * ```js - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; - * ``` - * - * Calling these methods invoke an {@link ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class. The actions `save`, `remove` and `delete` are available on it - * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, - * read, update, delete) on server-side data like this: - * ```js - * var User = $resource('/user/:userId', {userId:'@id'}); - * var user = User.get({userId:123}, function() { - * user.abc = true; - * user.$save(); - * }); - * ``` - * - * It is important to realize that invoking a $resource object method immediately returns an - * empty reference (object or array depending on `isArray`). Once the data is returned from the - * server the existing reference is populated with the actual data. This is a useful trick since - * usually the resource is assigned to a model which is then rendered by the view. Having an empty - * object results in no rendering, once the data arrives from the server then the object is - * populated with the data and the view automatically re-renders itself showing the new data. This - * means that in most cases one never has to write a callback function for the action methods. - * - * The action methods on the class object or instance object can be invoked with the following - * parameters: - * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` - * - * Success callback is called with (value, responseHeaders) arguments. Error callback is called - * with (httpResponse) argument. - * - * Class actions return empty instance (with additional properties below). - * Instance actions return promise of the action. - * - * The Resource instances and collection have these additional properties: - * - * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this - * instance or collection. - * - * On success, the promise is resolved with the same resource instance or collection object, - * updated with data from server. This makes it easy to use in - * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view - * rendering until the resource(s) are loaded. - * - * On failure, the promise is resolved with the {@link ng.$http http response} object, without - * the `resource` property. - * - * If an interceptor object was provided, the promise will instead be resolved with the value - * returned by the interceptor. - * - * - `$resolved`: `true` after first server interaction is completed (either with success or - * rejection), `false` before that. Knowing if the Resource has been resolved is useful in - * data-binding. - * - * @example - * - * # Credit card resource - * - * ```js - // Define CreditCard class - var CreditCard = $resource('/user/:userId/card/:cardId', - {userId:123, cardId:'@id'}, { - charge: {method:'POST', params:{charge:true}} - }); - - // We can retrieve a collection from the server - var cards = CreditCard.query(function() { - // GET: /user/123/card - // server returns: [ {id:456, number:'1234', name:'Smith'} ]; - - var card = cards[0]; - // each item is an instance of CreditCard - expect(card instanceof CreditCard).toEqual(true); - card.name = "J. Smith"; - // non GET methods are mapped onto the instances - card.$save(); - // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} - // server returns: {id:456, number:'1234', name: 'J. Smith'}; - - // our custom method is mapped as well. - card.$charge({amount:9.99}); - // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} - }); - - // we can create an instance as well - var newCard = new CreditCard({number:'0123'}); - newCard.name = "Mike Smith"; - newCard.$save(); - // POST: /user/123/card {number:'0123', name:'Mike Smith'} - // server returns: {id:789, number:'0123', name: 'Mike Smith'}; - expect(newCard.id).toEqual(789); - * ``` - * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. - * - * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and - * `headers`. - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. - - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user) { - user.abc = true; - user.$save(); - }); - ``` - * - * It's worth noting that the success callback for `get`, `query` and other methods gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: - * - ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(u, getResponseHeaders){ - u.abc = true; - u.$save(function(u, putResponseHeaders) { - //u => saved user object - //putResponseHeaders => $http header getter - }); - }); - ``` - * - * You can also access the raw `$http` promise via the `$promise` property on the object returned - * - ``` - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}) - .$promise.then(function(user) { - $scope.user = user; - }); - ``` - - * # Creating a custom 'PUT' request - * In this example we create a custom method on our resource to make a PUT request - * ```js - * var app = angular.module('app', ['ngResource', 'ngRoute']); - * - * // Some APIs expect a PUT request in the format URL/object/ID - * // Here we are creating an 'update' method - * app.factory('Notes', ['$resource', function($resource) { - * return $resource('/notes/:id', null, - * { - * 'update': { method:'PUT' } - * }); - * }]); - * - * // In our controller we get the ID from the URL using ngRoute and $routeParams - * // We pass in $routeParams and our Notes factory along with $scope - * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', - function($scope, $routeParams, Notes) { - * // First get a note object from the factory - * var note = Notes.get({ id:$routeParams.id }); - * $id = note.id; - * - * // Now call update passing in the ID first then the object you are updating - * Notes.update({ id:$id }, note); - * - * // This will PUT /notes/ID with the note object in the request payload - * }]); - * ``` - */ -angular.module('ngResource', ['ng']). - provider('$resource', function () { - var provider = this; - - this.defaults = { - // Strip slashes by default - stripTrailingSlashes: true, - - // Default actions configuration - actions: { - 'get': {method: 'GET'}, - 'save': {method: 'POST'}, - 'query': {method: 'GET', isArray: true}, - 'remove': {method: 'DELETE'}, - 'delete': {method: 'DELETE'} - } - }; - - this.$get = ['$http', '$q', function ($http, $q) { - - var noop = angular.noop, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy, - isFunction = angular.isFunction; - - /** - * We need our custom method because encodeURIComponent is too aggressive and doesn't follow - * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set - * (pchar) allowed in path segments: - * segment = *pchar - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * pct-encoded = "%" HEXDIG HEXDIG - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriSegment(val) { - return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); - } - - - /** - * This method is intended for encoding *key* or *value* parts of query component. We need a - * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't - * have to be encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); - } - - function Route(template, defaults) { - this.template = template; - this.defaults = extend({}, provider.defaults, defaults); - this.urlParams = {}; - } - - Route.prototype = { - setUrlParams: function (config, params, actionUrl) { - var self = this, - url = actionUrl || self.template, - val, - encodedVal; - - var urlParams = self.urlParams = {}; - forEach(url.split(/\W/), function (param) { - if (param === 'hasOwnProperty') { - throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); - } - if (!(new RegExp("^\\d+$").test(param)) && param && - (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { - urlParams[param] = true; - } - }); - url = url.replace(/\\:/g, ':'); - - params = params || {}; - forEach(self.urlParams, function (_, urlParam) { - val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; - if (angular.isDefined(val) && val !== null) { - encodedVal = encodeUriSegment(val); - url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function (match, p1) { - return encodedVal + p1; - }); - } else { - url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function (match, - leadingSlashes, tail) { - if (tail.charAt(0) == '/') { - return tail; - } else { - return leadingSlashes + tail; - } - }); - } - }); - - // strip trailing slashes and set the url (unless this behavior is specifically disabled) - if (self.defaults.stripTrailingSlashes) { - url = url.replace(/\/+$/, '') || '/'; - } - - // then replace collapse `/.` if found in the last URL path segment before the query - // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` - url = url.replace(/\/\.(?=\w+($|\?))/, '.'); - // replace escaped `/\.` with `/.` - config.url = url.replace(/\/\\\./, '/.'); - - - // set params - delegate param encoding to $http - forEach(params, function (value, key) { - if (!self.urlParams[key]) { - config.params = config.params || {}; - config.params[key] = value; - } - }); - } - }; - - - function resourceFactory(url, paramDefaults, actions, options) { - var route = new Route(url, options); - - actions = extend({}, provider.defaults.actions, actions); - - function extractParams(data, actionParams) { - var ids = {}; - actionParams = extend({}, paramDefaults, actionParams); - forEach(actionParams, function (value, key) { - if (isFunction(value)) { value = value(); } - ids[key] = value && value.charAt && value.charAt(0) == '@' ? - lookupDottedPath(data, value.substr(1)) : value; - }); - return ids; - } - - function defaultResponseInterceptor(response) { - return response.resource; - } - - function Resource(value) { - shallowClearAndCopy(value || {}, this); - } - - Resource.prototype.toJSON = function () { - var data = extend({}, this); - delete data.$promise; - delete data.$resolved; - return data; - }; - - forEach(actions, function (action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); - - Resource[name] = function (a1, a2, a3, a4) { - var params = {}, data, success, error; - - /* jshint -W086 */ /* (purposefully fall through case statements) */ - switch (arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } - - success = a2; - error = a3; - //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (hasBody) data = a1; - else params = a1; - break; - case 0: break; - default: - throw $resourceMinErr('badargs', - "Expected up to 4 arguments [params, data, success, error], got {0} arguments", - arguments.length); - } - /* jshint +W086 */ /* (purposefully fall through case statements) */ - - var isInstanceCall = this instanceof Resource; - var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); - var httpConfig = {}; - var responseInterceptor = action.interceptor && action.interceptor.response || - defaultResponseInterceptor; - var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; - - forEach(action, function (value, key) { - if (key != 'params' && key != 'isArray' && key != 'interceptor') { - httpConfig[key] = copy(value); - } - }); - - if (hasBody) httpConfig.data = data; - route.setUrlParams(httpConfig, - extend({}, extractParams(data, action.params || {}), params), - action.url); - - var promise = $http(httpConfig).then(function (response) { - var data = response.data, - promise = value.$promise; - - if (data) { - // Need to convert action.isArray to boolean in case it is undefined - // jshint -W018 - if (angular.isArray(data) !== (!!action.isArray)) { - throw $resourceMinErr('badcfg', - 'Error in resource configuration. Expected ' + - 'response to contain an {0} but got an {1}', - action.isArray ? 'array' : 'object', - angular.isArray(data) ? 'array' : 'object'); - } - // jshint +W018 - if (action.isArray) { - value.length = 0; - forEach(data, function (item) { - if (typeof item === "object") { - value.push(new Resource(item)); - } else { - // Valid JSON values may be string literals, and these should not be converted - // into objects. These items will not have access to the Resource prototype - // methods, but unfortunately there - value.push(item); - } - }); - } else { - shallowClearAndCopy(data, value); - value.$promise = promise; - } - } - - value.$resolved = true; - - response.resource = value; - - return response; - }, function (response) { - value.$resolved = true; - - (error || noop)(response); - - return $q.reject(response); - }); - - promise = promise.then( - function (response) { - var value = responseInterceptor(response); - (success || noop)(value, response.headers); - return value; - }, - responseErrorInterceptor); - - if (!isInstanceCall) { - // we are creating instance / collection - // - set the initial promise - // - return the instance / collection - value.$promise = promise; - value.$resolved = false; - - return value; - } - - // instance call - return promise; - }; - - - Resource.prototype['$' + name] = function (params, success, error) { - if (isFunction(params)) { - error = success; success = params; params = {}; - } - var result = Resource[name].call(this, params, this, success, error); - return result.$promise || result; - }; - }); - - Resource.bind = function (additionalParamDefaults) { - return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; - - return Resource; - } - - return resourceFactory; - }]; - }); - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-resource.min.js b/src/main/webapp/new/lib/angular/angular-resource.min.js deleted file mode 100644 index 7a86a1d3..00000000 --- a/src/main/webapp/new/lib/angular/angular-resource.min.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(H,d,A){'use strict';function C(g,q){q=q||{};d.forEach(q,function(d,h){delete q[h]});for(var h in g)!g.hasOwnProperty(h)||"$"===h.charAt(0)&&"$"===h.charAt(1)||(q[h]=g[h]);return q}var w=d.$$minErr("$resource"),B=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;d.module("ngResource",["ng"]).provider("$resource",function(){var g=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}}; -this.$get=["$http","$q",function(q,h){function t(d,k){this.template=d;this.defaults=s({},g.defaults,k);this.urlParams={}}function v(x,k,l,m){function f(b,c){var f={};c=s({},k,c);r(c,function(a,c){u(a)&&(a=a());var d;if(a&&a.charAt&&"@"==a.charAt(0)){d=b;var e=a.substr(1);if(null==e||""===e||"hasOwnProperty"===e||!B.test("."+e))throw w("badmember",e);for(var e=e.split("."),n=0,k=e.length;n - * - * See {@link ngSanitize.$sanitize `$sanitize`} for usage. - */ - -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - -/** - * @ngdoc service - * @name $sanitize - * @kind function - * - * @description - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. - * - * @param {string} html Html input. - * @returns {string} Sanitized html. - * - * @example - - - -
    - Snippet: - - - - - - - - - - - - - - - - - - - - - - - - - -
    DirectiveHowSourceRendered
    ng-bind-htmlAutomatically uses $sanitize
    <div ng-bind-html="snippet">
    </div>
    ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value -
    <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
    -</div>
    -
    ng-bindAutomatically escapes
    <div ng-bind="snippet">
    </div>
    -
    -
    - - it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('

    an html\nclick here\nsnippet

    '); - }); - - it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). - toBe("

    an html\n" + - "click here\n" + - "snippet

    "); - }); - - it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new text'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('new text'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( - 'new text'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( - "new <b onclick=\"alert(1)\">text</b>"); - }); -
    -
    - */ -function $SanitizeProvider() { - this.$get = ['$$sanitizeUri', function($$sanitizeUri) { - return function(html) { - var buf = []; - htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe/.test($$sanitizeUri(uri, isImage)); - })); - return buf.join(''); - }; - }]; -} - -function sanitizeText(chars) { - var buf = []; - var writer = htmlSanitizeWriter(buf, angular.noop); - writer.chars(chars); - return buf.join(''); -} - - -// Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = - /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, - END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - DOCTYPE_REGEXP = /]*?)>/i, - CDATA_REGEXP = //g, - SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - // Match everything outside of normal chars and " (quote character) - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; - - -// Good source of info about elements and attributes -// http://dev.w3.org/html5/spec/Overview.html#semantics -// http://simon.html5.org/html-elements - -// Safe Void Elements - HTML5 -// http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); - -// Elements that you can, intentionally, leave open (and which close themselves) -// http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = angular.extend({}, - optionalEndTagInlineElements, - optionalEndTagBlockElements); - -// Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + - "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + - "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); - -// Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + - "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + - "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); - - -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); - -var validElements = angular.extend({}, - voidElements, - blockElements, - inlineElements, - optionalEndTagElements); - -//Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); -var validAttrs = angular.extend({}, uriAttrs, makeMap( - 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ - 'valign,value,vspace,width')); - -function makeMap(str) { - var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; - return obj; -} - - -/** - * @example - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - * @param {string} html string - * @param {object} handler - */ -function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[ stack.length - 1 ]; }; - - while ( html ) { - text = ''; - chars = true; - - // Make sure we're not in a script or style element - if ( !stack.last() || !specialElements[ stack.last() ] ) { - - // Comment - if ( html.indexOf("", index) === index) { - if (handler.comment) handler.comment( html.substring( 4, index ) ); - html = html.substring( index + 3 ); - chars = false; - } - // DOCTYPE - } else if ( DOCTYPE_REGEXP.test(html) ) { - match = html.match( DOCTYPE_REGEXP ); - - if ( match ) { - html = html.replace( match[0], ''); - chars = false; - } - // end tag - } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { - match = html.match( END_TAG_REGEXP ); - - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( END_TAG_REGEXP, parseEndTag ); - chars = false; - } - - // start tag - } else if ( BEGIN_TAG_REGEXP.test(html) ) { - match = html.match( START_TAG_REGEXP ); - - if ( match ) { - // We only have a valid start-tag if there is a '>'. - if ( match[4] ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); - } - chars = false; - } else { - // no ending tag found --- this piece should be encoded as an entity. - text += '<'; - html = html.substring(1); - } - } - - if ( chars ) { - index = html.indexOf("<"); - - text += index < 0 ? html : html.substring( 0, index ); - html = index < 0 ? "" : html.substring( index ); - - if (handler.chars) handler.chars( decodeEntities(text) ); - } - - } else { - html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text){ - text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars( decodeEntities(text) ); - - return ""; - }); - - parseEndTag( "", stack.last() ); - } - - if ( html == last ) { - throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + - "of html: {0}", html); - } - last = html; - } - - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag( tag, tagName, rest, unary ) { - tagName = angular.lowercase(tagName); - if ( blockElements[ tagName ] ) { - while ( stack.last() && inlineElements[ stack.last() ] ) { - parseEndTag( "", stack.last() ); - } - } - - if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { - parseEndTag( "", tagName ); - } - - unary = voidElements[ tagName ] || !!unary; - - if ( !unary ) - stack.push( tagName ); - - var attrs = {}; - - rest.replace(ATTR_REGEXP, - function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { - var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start( tagName, attrs, unary ); - } - - function parseEndTag( tag, tagName ) { - var pos = 0, i; - tagName = angular.lowercase(tagName); - if ( tagName ) - // Find the closest opened tag of the same type - for ( pos = stack.length - 1; pos >= 0; pos-- ) - if ( stack[ pos ] == tagName ) - break; - - if ( pos >= 0 ) { - // Close all the open elements, up the stack - for ( i = stack.length - 1; i >= pos; i-- ) - if (handler.end) handler.end( stack[ i ] ); - - // Remove the open elements from the stack - stack.length = pos; - } - } -} - -var hiddenPre=document.createElement("pre"); -var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function decodeEntities(value) { - if (!value) { return ''; } - - // Note: IE8 does not preserve spaces at the start/end of innerHTML - // so we must capture them and reattach them afterward - var parts = spaceRe.exec(value); - var spaceBefore = parts[1]; - var spaceAfter = parts[3]; - var content = parts[2]; - if (content) { - hiddenPre.innerHTML=content.replace(//g, '>'); -} - -/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function htmlSanitizeWriter(buf, uriValidator){ - var ignore = false; - var out = angular.bind(buf, buf.push); - return { - start: function(tag, attrs, unary){ - tag = angular.lowercase(tag); - if (!ignore && specialElements[tag]) { - ignore = tag; - } - if (!ignore && validElements[tag] === true) { - out('<'); - out(tag); - angular.forEach(attrs, function(value, key){ - var lkey=angular.lowercase(key); - var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); - if (validAttrs[lkey] === true && - (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { - out(' '); - out(key); - out('="'); - out(encodeEntities(value)); - out('"'); - } - }); - out(unary ? '/>' : '>'); - } - }, - end: function(tag){ - tag = angular.lowercase(tag); - if (!ignore && validElements[tag] === true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars){ - if (!ignore) { - out(encodeEntities(chars)); - } - } - }; -} - - -// define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); - -/* global sanitizeText: false */ - -/** - * @ngdoc filter - * @name linky - * @kind function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * Requires the {@link ngSanitize `ngSanitize`} module to be installed. - * - * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. - * - * @usage - - * - * @example - - - -
    - Snippet: - - - - - - - - - - - - - - - - - - - - - -
    FilterSourceRendered
    linky filter -
    <div ng-bind-html="snippet | linky">
    </div>
    -
    -
    -
    linky target -
    <div ng-bind-html="snippetWithTarget | linky:'_blank'">
    </div>
    -
    -
    -
    no filter
    <div ng-bind="snippet">
    </div>
    - - - it('should linkify the snippet with urls', function() { - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); - }); - - it('should not linkify snippet without the linky filter', function() { - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new http://link.'); - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('new http://link.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) - .toBe('new http://link.'); - }); - - it('should work with the target property', function() { - expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); - }); - - - */ -angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { - var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, - MAILTO_REGEXP = /^mailto:/; - - return function(text, target) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; - i = match.index; - addText(raw.substr(0, i)); - addLink(url, match[0].replace(MAILTO_REGEXP, '')); - raw = raw.substring(i + match[0].length); - } - addText(raw); - return $sanitize(html.join('')); - - function addText(text) { - if (!text) { - return; - } - html.push(sanitizeText(text)); - } - - function addLink(url, text) { - html.push(''); - addText(text); - html.push(''); - } - }; -}]); - - -})(window, window.angular); diff --git a/src/main/webapp/new/lib/angular/angular-sanitize.min.js b/src/main/webapp/new/lib/angular/angular-sanitize.min.js deleted file mode 100644 index 3f0b0b05..00000000 --- a/src/main/webapp/new/lib/angular/angular-sanitize.min.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - AngularJS v1.3.0-beta.17 - (c) 2010-2014 Google, Inc. http://angularjs.org - License: MIT -*/ -(function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var b;for(b=0;b=b;e--)d.end&&d.end(f[e]);f.length=b}}var c,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(c,a){a=a.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(a));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))c=a.indexOf("--",4),0<=c&&a.lastIndexOf("--\x3e",c)===c&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),l=!1);else if(z.test(a)){if(c= -a.match(z))a=a.replace(c[0],""),l=!1}else if(K.test(a)){if(c=a.match(A))a=a.substring(c[0].length),c[0].replace(A,e),l=!1}else L.test(a)&&((c=a.match(B))?(c[4]&&(a=a.substring(c[0].length),c[0].replace(B,b)),l=!1):(h+="<",a=a.substring(1)));l&&(c=a.indexOf("<"),h+=0>c?a:a.substring(0,c),a=0>c?"":a.substring(c),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var b=d[3];if(d=d[2])p.innerHTML=d.replace(//g,">")}function t(a,d){var b=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!b&&y[a]&&(b=a);b||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(b,f){var k=g.lowercase(f),l="img"===a&&"src"===k||"background"=== -k;!0!==Q[k]||!0===E[k]&&!d(b,l)||(e(" "),e(f),e('="'),e(C(b)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);b||!0!==D[a]||(e(""));a==b&&(b=!1)},chars:function(a){b||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^]*?)>/i, -J=/]/,b=/^mailto:/;return function(e,c){function l(a){a&&k.push(F(a))}function f(a,b){k.push("');l(b);k.push("")} -if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(b,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular); -//# sourceMappingURL=angular-sanitize.min.js.map diff --git a/src/main/webapp/new/lib/angular/angular-sanitize.min.js.map b/src/main/webapp/new/lib/angular/angular-sanitize.min.js.map deleted file mode 100644 index 1b4a7508..00000000 --- a/src/main/webapp/new/lib/angular/angular-sanitize.min.js.map +++ /dev/null @@ -1,8 +0,0 @@ -{ -"version":3, -"file":"angular-sanitize.min.js", -"lineCount":14, -"mappings":"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", -"sources":["angular-sanitize.js"], -"names":["window","angular","undefined","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","makeMap","str","obj","items","split","i","length","htmlParser","html","handler","parseStartTag","tag","tagName","rest","unary","lowercase","blockElements","stack","last","inlineElements","parseEndTag","optionalEndTagElements","voidElements","push","attrs","replace","ATTR_REGEXP","match","name","doubleQuotedValue","singleQuotedValue","unquotedValue","decodeEntities","start","pos","end","index","text","stack.last","specialElements","RegExp","all","COMMENT_REGEXP","CDATA_REGEXP","indexOf","lastIndexOf","comment","substring","DOCTYPE_REGEXP","test","BEGING_END_TAGE_REGEXP","END_TAG_REGEXP","BEGIN_TAG_REGEXP","START_TAG_REGEXP","$sanitizeMinErr","value","parts","spaceRe","exec","spaceBefore","spaceAfter","content","hiddenPre","innerHTML","textContent","innerText","encodeEntities","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","uriValidator","ignore","out","bind","validElements","forEach","key","lkey","isImage","validAttrs","uriAttrs","$$minErr","optionalEndTagBlockElements","optionalEndTagInlineElements","extend","document","createElement","module","provider","$SanitizeProvider","$get","$$sanitizeUri","uri","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","target","addText","addLink","url","isDefined","raw","substr"] -} diff --git a/src/main/webapp/new/lib/angular/angular-scenario.js b/src/main/webapp/new/lib/angular/angular-scenario.js deleted file mode 100644 index 9cc94eb5..00000000 --- a/src/main/webapp/new/lib/angular/angular-scenario.js +++ /dev/null @@ -1,35363 +0,0 @@ -/*! - * jQuery JavaScript Library v1.10.2 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-07-03T13:48Z - */ -(function( window, undefined ) {'use strict'; - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -// - -var - // The deferred used on DOM ready - readyList, - - // A central reference to the root jQuery(document) - rootjQuery, - - // Support: IE<10 - // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` - core_strundefined = typeof undefined, - - // Use the correct document accordingly with window argument (sandbox) - location = window.location, - document = window.document, - docElem = document.documentElement, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // [[Class]] -> type pairs - class2type = {}, - - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], - - core_version = "1.10.2", - - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }, - - // The ready event handler - completed = function( event ) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: core_version, - - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return core_slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; - }, - - slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), - - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; - }, - - isPlainObject: function( obj ) { - var key; - - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !core_hasOwn.call(obj, "constructor") && - !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if ( jQuery.support.ownLast ) { - for ( key in obj ) { - return core_hasOwn.call( obj, key ); - } - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - for ( key in obj ) {} - - return key === undefined || core_hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw new Error( msg ); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - if ( scripts ) { - jQuery( scripts ).remove(); - } - return jQuery.merge( [], parsed.childNodes ); - }, - - parseJSON: function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - } - } - } - - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? - function( text ) { - return text == null ? - "" : - core_trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - core_push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( core_indexOf ) { - return core_indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; - - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var retVal, - ret = [], - i = 0, - length = elems.length; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return core_concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var args, proxy, tmp; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = core_slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations. - // Note: this method belongs to the css module but it's needed here for the support module. - // If support gets modularized, this method should be moved back to the css module. - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } -}); - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - var length = obj.length, - type = jQuery.type( obj ); - - if ( jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); -/*! - * Sizzle CSS Selector Engine v1.10.2 - * http://sizzlejs.com/ - * - * Copyright 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-07-03 - */ -(function( window, undefined ) { - -var i, - support, - cachedruns, - Expr, - getText, - isXML, - compile, - outermostContext, - sortInput, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - hasDuplicate = false, - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - return 0; - }, - - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rsibling = new RegExp( whitespace + "*[+~]" ), - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rescape = /'|\\/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - // BMP codepoint - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } - - if ( documentIsHTML && !seed ) { - - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && context.parentNode || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return !!fn( div ); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if ( div.parentNode ) { - div.parentNode.removeChild( div ); - } - // release memory in IE - div = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = attrs.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - ( ~b.sourceIndex || MAX_NEGATIVE ) - - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Detect xml - * @param {Element|Object} elem An element or a document - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsHTML = !isXML( doc ); - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent.attachEvent && parent !== parent.top ) { - parent.attachEvent( "onbeforeunload", function() { - setDocument(); - }); - } - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) - support.attributes = assert(function( div ) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Check if getElementsByClassName can be trusted - support.getElementsByClassName = assert(function( div ) { - div.innerHTML = "
    "; - - // Support: Safari<4 - // Catch class over-caching - div.firstChild.className = "i"; - // Support: Opera<10 - // Catch gEBCN failure to find non-leading classes - return div.getElementsByClassName("i").length === 2; - }); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function( div ) { - docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; - }); - - // ID find and filter - if ( support.getById ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && documentIsHTML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - // Support: IE6/7 - // getElementById is not reliable as a find shortcut - delete Expr.find["ID"]; - - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See http://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function( div ) { - - // Support: Opera 10-12/IE8 - // ^= $= *= and empty values - // Should not select anything - // Support: Windows 8 Native Apps - // The type attribute is restricted during .innerHTML assignment - var input = doc.createElement("input"); - input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "t", "" ); - - if ( div.querySelectorAll("[t^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = docElem.compareDocumentPosition ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); - - if ( compare ) { - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === doc || contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } - - // Not directly comparable, sort on existence of method - return a.compareDocumentPosition ? -1 : 1; - } : - function( a, b ) { - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - if ( support.matchesSelector && documentIsHTML && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch(e) {} - } - - return Sizzle( expr, document, null, [elem] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val === undefined ? - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null : - val; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[5] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[3] && match[4] !== undefined ) { - match[2] = match[4]; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( tokens = [] ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } - } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { - var elem, j, matcher, - setMatched = [], - matchedCount = 0, - i = "0", - unmatched = seed && [], - outermost = expandContext != null, - contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); - - if ( outermost ) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); - } - i = group.length; - while ( i-- ) { - cached = matcherFromTokens( group[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - } - return cached; -}; - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - match = tokenize( selector ); - - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - } - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - } - - // Compile and execute a filtering function - // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( - seed, - context, - !documentIsHTML, - results, - rsibling.test( selector ) - ); - return results; -} - -// One-time assignments - -// Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -// Support: Chrome<14 -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( div1 ) { - // Should return 1, but returns 4 (following) - return div1.compareDocumentPosition( document.createElement("div") ) & 1; -}); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( div ) { - div.innerHTML = ""; - return div.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( div ) { - div.innerHTML = ""; - div.firstChild.setAttribute( "value", "" ); - return div.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( div ) { - return div.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - elem[ name ] === true ? name.toLowerCase() : null; - } - }); -} - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})( window ); -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = core_slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { - deferred.notifyWith( contexts, values ); - } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); -jQuery.support = (function( support ) { - - var all, a, input, select, fragment, opt, eventName, isSupported, i, - div = document.createElement("div"); - - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = "
    a"; - - // Finish early in limited (non-browser) environments - all = div.getElementsByTagName("*") || []; - a = div.getElementsByTagName("a")[ 0 ]; - if ( !a || !a.style || !all.length ) { - return support; - } - - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px;float:left;opacity:.5"; - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; - - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName("tbody").length; - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !!div.getElementsByTagName("link").length; - - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test( a.getAttribute("style") ); - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - support.opacity = /^0.5/.test( a.style.opacity ); - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !!a.style.cssFloat; - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !!input.value; - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; - - // Tests for enctype support on a form (#6743) - support.enctype = !!document.createElement("form").enctype; - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; - - // Will be defined later - support.inlineBlockNeedsLayout = false; - support.shrinkWrapBlocks = false; - support.pixelPosition = false; - support.deleteExpando = true; - support.noCloneEvent = true; - support.reliableMarginRight = true; - support.boxSizingReliable = true; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); - - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); - - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Support: IE<9 - // Iteration over object's inherited properties before its own. - for ( i in jQuery( support ) ) { - break; - } - support.ownLast = i !== "0"; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body - return; - } - - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
    t
    "; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior. - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - - // Workaround failing boxSizing test due to offsetWidth returning wrong value - // with some non-1 values of body zoom, ticket #13543 - jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { - support.boxSizing = div.offsetWidth === 4; - }); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); - } - - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
    "; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild( container ); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; -})({}); - -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var ret, thisCache, - internalKey = jQuery.expando, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( typeof name === "string" ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - - i = name.length; - while ( i-- ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - /* jshint eqeqeq: true */ - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } -} - -jQuery.extend({ - cache: {}, - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "applet": true, - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, - - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, - - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var attrs, name, - data = null, - i = 0, - elem = this[0]; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; - - if ( name.indexOf("data-") === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - - dataAttr( elem, name, data[ name ] ); - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - return arguments.length > 1 ? - - // Sets one value - this.each(function() { - jQuery.data( this, key, value ); - }) : - - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n\f]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); - - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; - - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - classNames = value.match( core_rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var ret, hooks, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // Use proper attribute retrieval(#6932, #12072) - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - elem.text; - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { - optionSet = true; - } - } - - // force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attr: function( elem, name, value ) { - var hooks, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( jQuery.expr.match.bool.test( name ) ) { - // Set corresponding property to false - if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - elem[ propName ] = false; - // Support: IE<9 - // Also clear defaultChecked/defaultSelected (if appropriate) - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? - ret : - ( elem[ name ] = value ); - - } else { - return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? - ret : - elem[ name ]; - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - return tabindex ? - parseInt( tabindex, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - -1; - } - } - } -}); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; - - jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? - function( elem, name, isXML ) { - var fn = jQuery.expr.attrHandle[ name ], - ret = isXML ? - undefined : - /* jshint eqeqeq: false */ - (jQuery.expr.attrHandle[ name ] = undefined) != - getter( elem, name, isXML ) ? - - name.toLowerCase() : - null; - jQuery.expr.attrHandle[ name ] = fn; - return ret; - } : - function( elem, name, isXML ) { - return isXML ? - undefined : - elem[ jQuery.camelCase( "default-" + name ) ] ? - name.toLowerCase() : - null; - }; -}); - -// fix oldIE attroperties -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = { - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute( name ) ? - value : - undefined; - } - }; - jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = - // Some attributes are constructed with empty-string values when not defined - function( elem, name, isXML ) { - var ret; - return isXML ? - undefined : - (ret = elem.getAttributeNode( name )) && ret.value !== "" ? - ret.value : - null; - }; - jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ret.specified ? - ret.value : - undefined; - }, - set: nodeHook.set - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }; - }); -} - - -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !jQuery.support.hrefNormalized ) { - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }; -} - -jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -}); - -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - -// Radios and checkboxes getter/setter -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }; - if ( !jQuery.support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } -}); -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - /* jshint eqeqeq: false */ - for ( ; cur != this; cur = cur.parentNode || this ) { - /* jshint eqeqeq: true */ - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Even when returnValue equals to undefined Firefox will still show alert - if ( event.result !== undefined ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === core_strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); -var isSimple = /^.[^:#\[\.,]*$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var i, - ret = [], - self = this, - len = self.length; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, - - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; - - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); - }, - - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - ret = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { - // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { - - cur = ret.push( cur ); - break; - } - } - } - - return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( jQuery.unique(all) ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); - - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - if ( this.length > 1 ) { - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - ret = jQuery.unique( ret ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - } - - return this.pushStack( ret ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : - jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - })); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - /* jshint -W018 */ - return !!qualifier.call( elem, i, elem ) !== not; - }); - - } - - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - }); - - } - - if ( typeof qualifier === "string" ) { - if ( isSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - qualifier = jQuery.filter( qualifier, elements ); - } - - return jQuery.grep( elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; - }); -} -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "" ], - legend: [ 1, "
    ", "
    " ], - area: [ 1, "", "" ], - param: [ 1, "", "" ], - thead: [ 1, "", "
    " ], - tr: [ 2, "", "
    " ], - col: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -jQuery.fn.extend({ - text: function( value ) { - return jQuery.access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - append: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - // keepData is for internal use only--do not document - remove: function( selector, keepData ) { - var elem, - elems = selector ? jQuery.filter( selector, this ) : this, - i = 0; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function () { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return jQuery.access( this, function( value ) { - var elem = this[0] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1>" ); - - try { - for (; i < l; i++ ) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch(e) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var - // Snapshot the DOM in case .domManip sweeps something relevant into its fragment - args = jQuery.map( this, function( elem ) { - return [ elem.nextSibling, elem.parentNode ]; - }), - i = 0; - - // Make the changes, replacing each context element with the new content - this.domManip( arguments, function( elem ) { - var next = args[ i++ ], - parent = args[ i++ ]; - - if ( parent ) { - // Don't use the snapshot next if it has moved (#13810) - if ( next && next.parentNode !== parent ) { - next = this.nextSibling; - } - jQuery( this ).remove(); - parent.insertBefore( elem, next ); - } - // Allow new content to include elements from the context set - }, true ); - - // Force removal if there was no new content (e.g., from empty arguments) - return i ? this : this.remove(); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, callback, allowIntersection ) { - - // Flatten any nested arrays - args = core_concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[0] = value.call( this, index, self.html() ); - } - self.domManip( args, callback, allowIntersection ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( this[i], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Hope ajax is available... - jQuery._evalUrl( node.src ); - } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } -}); - -// Support: IE<8 -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild( elem.ownerDocument.createElement("tbody") ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - core_push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( manipulation_rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment( context ), - - nodes = [], - i = 0; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - - // Remove IE's autoinserted from table fragments - if ( !jQuery.support.tbody ) { - - // String was a , *may* have spurious - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare or - wrap[1] === "
    " && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !jQuery.support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = jQuery.support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; - - } else if ( typeof elem.removeAttribute !== core_strundefined ) { - elem.removeAttribute( internalKey ); - - } else { - elem[ internalKey ] = null; - } - - core_deletedIds.push( id ); - } - } - } - } - }, - - _evalUrl: function( url ) { - return jQuery.ajax({ - url: url, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); - } -}); -jQuery.fn.extend({ - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - } -}); -var iframe, getStyles, curCSS, - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - rposition = /^(top|right|bottom|left)$/, - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rmargin = /^margin/, - rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), - rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), - elemdisplay = { BODY: "block" }, - - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: 0, - fontWeight: 400 - }, - - cssExpand = [ "Top", "Right", "Bottom", "Left" ], - cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; - -// return a css property mapped to a potentially vendor prefixed property -function vendorPropName( style, name ) { - - // shortcut for names that are not vendor prefixed - if ( name in style ) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in style ) { - return name; - } - } - - return origName; -} - -function isHidden( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); -} - -function showHide( elements, show ) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - values[ index ] = jQuery._data( elem, "olddisplay" ); - display = elem.style.display; - if ( show ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !values[ index ] && display === "none" ) { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); - } - } else { - - if ( !values[ index ] ) { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( index = 0; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - if ( !show || elem.style.display === "none" || elem.style.display === "" ) { - elem.style.display = show ? values[ index ] || "" : "none"; - } - } - - return elements; -} - -jQuery.fn.extend({ - css: function( name, value ) { - return jQuery.access( this, function( elem, name, value ) { - var len, styles, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each(function() { - if ( isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - -jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if ( type === "string" && (ret = rrelNum.exec( value )) ) { - value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that NaN and null values aren't set. See: #7116 - if ( value == null || type === "number" && isNaN( value ) ) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( type === "number" && !jQuery.cssNumber[ origName ] ) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Wrapped to prevent IE from throwing errors when 'invalid' values are provided - // Fixes bug #5509 - try { - style[ name ] = value; - } catch(e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var num, val, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - //convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; - } - return val; - } -}); - -// NOTE: we've included the "window" in window.getComputedStyle -// because jsdom on node.js will break without it. -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - return window.getComputedStyle( elem, null ); - }; - - curCSS = function( elem, name, _computed ) { - var width, minWidth, maxWidth, - computed = _computed || getStyles( elem ), - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, - style = elem.style; - - if ( computed ) { - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; - - curCSS = function( elem, name, _computed ) { - var left, rs, rsLeft, - computed = _computed || getStyles( elem ), - ret = computed ? computed[ name ] : undefined, - style = elem.style; - - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; - } - } - - return ret === "" ? "auto" : ret; - }; -} - -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -// Try to determine the default display value of an element -function css_defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - // Use the already-created iframe if possible - iframe = ( iframe || - jQuery("