├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src └── main ├── docker └── Dockerfile ├── java └── simpleblog │ ├── Application.java │ ├── SecurityConfiguration.java │ ├── WebMvcConfiguration.java │ ├── controller │ ├── AuthController.java │ └── PostController.java │ └── domain │ ├── BlogService.java │ ├── EntryDoesNotExistException.java │ ├── Post.java │ ├── PostRepository.java │ ├── PostTitleValidator.java │ └── Title.java └── resources ├── logback.xml ├── simpleblog ├── configuration │ ├── security-config.properties │ └── webmvc-config.properties └── view │ ├── layout.html │ ├── login.html │ └── posts.html └── static ├── css ├── simpleblog.css └── theme.bootstrap.css └── js ├── jquery.tablesorter.min.js ├── jquery.tablesorter.widgets.js └── simpleblog.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | build/ 3 | .gradle/ 4 | 5 | # Eclipse 6 | bin/ 7 | .cache 8 | .classpath 9 | .project 10 | .settings 11 | .groovy 12 | 13 | # IntelliJ 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | out/ 19 | 20 | # Mac 21 | .DS_Store 22 | 23 | # Maven 24 | /target/ 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #simple web app with spring boot 2 | 3 | This project contains a simple web application demo, which is build with spring boot and some common spring components. 4 | 5 | The project is based on: 6 | * spring-boot 7 | * spring-webmvc 8 | * spring-security 9 | * spring-data-jpa 10 | * tomcat 11 | * thymeleaf 12 | * hibernate-validator 13 | 14 | ## Building Spring Boot Application 15 | 16 | The spring demo application can be build with maven or gradle. To build the application as executable JAR use the maven goal: 17 | 18 | mvn package 19 | 20 | The application can be started by: 21 | 22 | java -jar target/simpleweb-springboot-*.jar 23 | 24 | With Gradle the application can be build by the following Gradle command: 25 | 26 | gradlew build 27 | 28 | Now the application can be started by the following command 29 | 30 | java -jar build/libs/simpleweb-springboot.jar 31 | 32 | ## Hot Deployment with Spring loaded and JHipster 33 | 34 | Since Spring Boot version 1.3.x spring loaded is not needed anymore. 35 | To hot reload the application [Spring DevTools](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-devtools) now can be used. 36 | 37 | Spring Boot 1.3 DevTools includes an embedded LiveReload server, to get a browser plugin see [here](http://livereload.com/extensions/). 38 | 39 | ## Run Spring Boot Sample 40 | 41 | To start the web application invoke: 42 | 43 | ./gradlew bootRun 44 | 45 | Open the URL http://localhost:8080 in a web browser. Login with the username "spring" and password "boot". 46 | 47 | ## Import into Eclipse 48 | 49 | The eclipse project configurations files can be build with gradle, invoke gradle: 50 | 51 | ./gradlew eclipse 52 | 53 | Or invoke the maven goal: 54 | 55 | mvn eclipse:eclipse 56 | 57 | ## Build Docker Image 58 | 59 | To build the docker image with gradle invoke 60 | 61 | ./gradlew build buildDocker 62 | 63 | With maven the docker image can be build by invoke the maven goal 64 | 65 | mvn clean package docker:build 66 | 67 | Tutorial how to crate a docker image for a spring boot application see [here](https://spring.io/guides/gs/spring-boot-docker). 68 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.3.2.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "http://repo.spring.io/release" } 8 | maven { url "http://repo.spring.io/milestone" } 9 | maven { url "http://repo.spring.io/snapshot" } 10 | } 11 | dependencies { 12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 13 | classpath('se.transmode.gradle:gradle-docker:1.2') 14 | } 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'idea' 20 | apply plugin: 'spring-boot' 21 | apply plugin: 'docker' 22 | 23 | repositories { 24 | mavenCentral() 25 | maven { url "http://repo.spring.io/release" } 26 | maven { url "http://repo.spring.io/milestone" } 27 | maven { url "http://repo.spring.io/snapshot" } 28 | } 29 | 30 | group = 'tux2323' 31 | 32 | jar { 33 | baseName = 'simpleweb-springboot' 34 | version = '0.1.0' 35 | } 36 | 37 | dependencies { 38 | compile("javax.inject:javax.inject:1") 39 | compile("org.springframework.boot:spring-boot-starter-actuator") 40 | compile("org.springframework.boot:spring-boot-starter-web") 41 | compile("org.springframework.boot:spring-boot-starter-security") 42 | compile("org.springframework.boot:spring-boot-starter-thymeleaf") 43 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 44 | compile("org.springframework.boot:spring-boot-starter-logging") 45 | // Spring Boot DevTools for hot reload 46 | compile("org.springframework.boot:spring-boot-devtools") 47 | 48 | compile("org.hibernate:hibernate-validator") 49 | compile("org.hsqldb:hsqldb") 50 | compile("commons-lang:commons-lang:2.6") 51 | compile("commons-io:commons-io:2.4") 52 | 53 | compile("org.webjars:bootstrap:3.1.0") 54 | compile("org.webjars:jquery:2.1.0-2") 55 | } 56 | 57 | task wrapper(type: Wrapper) { 58 | gradleVersion = '2.3' 59 | } 60 | 61 | task buildDocker(type: Docker, dependsOn: build) { 62 | push = true 63 | applicationName = jar.baseName 64 | dockerfile = file('src/main/docker/Dockerfile') 65 | doFirst { 66 | copy { 67 | from jar 68 | into stageDir 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chbaranowski/simpleweb-springboot/7ec9cf0d9b550717ce56c35c57690e6a4c701ba0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 25 20:32:44 CET 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 1.3.2.RELEASE 9 | 10 | 11 | com.seitenbau.samples 12 | simpleweb-springboot 13 | 0.1.0 14 | 15 | 16 | tux2323 17 | 18 | 19 | 20 | 21 | javax.inject 22 | javax.inject 23 | 1 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-actuator 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-security 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-thymeleaf 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-data-jpa 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-logging 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-devtools 54 | 55 | 56 | 57 | org.hibernate 58 | hibernate-validator 59 | 60 | 61 | org.hsqldb 62 | hsqldb 63 | 64 | 65 | 66 | commons-lang 67 | commons-lang 68 | 2.6 69 | 70 | 71 | commons-io 72 | commons-io 73 | 2.4 74 | 75 | 76 | 77 | org.webjars 78 | bootstrap 79 | 3.1.0 80 | 81 | 82 | org.webjars 83 | jquery 84 | 2.1.0-2 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-starter-test 90 | test 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 100 | 101 | 102 | com.spotify 103 | docker-maven-plugin 104 | 0.4.0 105 | 106 | ${docker.image.prefix}/${project.artifactId} 107 | src/main/docker 108 | 109 | 110 | / 111 | ${project.build.directory} 112 | ${project.build.finalName}.jar 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | VOLUME /tmp 3 | ADD simpleweb-springboot-0.1.0.jar simpleweb-springboot.jar 4 | RUN bash -c 'touch /simpleweb-springboot.jar' 5 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/simpleweb-springboot.jar"] -------------------------------------------------------------------------------- /src/main/java/simpleblog/Application.java: -------------------------------------------------------------------------------- 1 | package simpleblog; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @EnableAutoConfiguration 10 | @ComponentScan 11 | public class Application { 12 | 13 | public static void main(String[] args) throws Exception { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package simpleblog; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | 12 | @Configuration 13 | @EnableWebSecurity 14 | @PropertySource("classpath:/simpleblog/configuration/security-config.properties") 15 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 16 | 17 | @Value("${default.username}") 18 | String defaultUsername; 19 | 20 | @Value("${default.password}") 21 | String defaultPassword; 22 | 23 | @Override 24 | protected void configure(HttpSecurity http) throws Exception { 25 | http 26 | .csrf() 27 | .disable() 28 | .authorizeRequests() 29 | .antMatchers("/**").hasRole("USER") 30 | .and() 31 | .formLogin() 32 | .loginPage("/login") 33 | .defaultSuccessUrl("/") 34 | .usernameParameter("usr") 35 | .passwordParameter("pwd") 36 | .permitAll() 37 | .and() 38 | .logout() 39 | .logoutUrl("/logout") 40 | .logoutSuccessUrl("/login") 41 | .permitAll(); 42 | } 43 | 44 | @Override 45 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 46 | auth 47 | .inMemoryAuthentication() 48 | .withUser(defaultUsername) 49 | .password(defaultPassword) 50 | .roles("USER"); 51 | } 52 | 53 | @Override 54 | public void configure(WebSecurity web) throws Exception { 55 | web 56 | .ignoring() 57 | .antMatchers("/webjars/**") 58 | .and() 59 | .ignoring() 60 | .antMatchers("/static/**"); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/WebMvcConfiguration.java: -------------------------------------------------------------------------------- 1 | package simpleblog; 2 | 3 | import javax.servlet.Filter; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.web.filter.CharacterEncodingFilter; 9 | import org.springframework.web.servlet.DispatcherServlet; 10 | import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; 11 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 12 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 13 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 14 | 15 | @Configuration 16 | @EnableWebMvc 17 | @PropertySource("classpath:/simpleblog/configuration/webmvc-config.properties") 18 | public class WebMvcConfiguration extends WebMvcConfigurerAdapter { 19 | 20 | @Override 21 | public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { 22 | configurer.enable(); 23 | } 24 | 25 | @Override 26 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 27 | registry 28 | .addResourceHandler("/webjars/**") 29 | .addResourceLocations("classpath:/META-INF/resources/webjars/"); 30 | registry 31 | .addResourceHandler("/static/**") 32 | .addResourceLocations("classpath:/static/"); 33 | } 34 | 35 | @Bean 36 | public DispatcherServlet dispatcherServlet() { 37 | return new DispatcherServlet(); 38 | } 39 | 40 | @Bean 41 | public Filter characterEncodingFilter() { 42 | CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); 43 | characterEncodingFilter.setEncoding("UTF-8"); 44 | return characterEncodingFilter; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package simpleblog.controller; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | 9 | @Controller 10 | public class AuthController { 11 | 12 | @RequestMapping(value = "/login", method = RequestMethod.GET) 13 | public String login(Map model) { 14 | return "login"; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/controller/PostController.java: -------------------------------------------------------------------------------- 1 | package simpleblog.controller; 2 | 3 | import javax.validation.Valid; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.orm.ObjectOptimisticLockingFailureException; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.ModelMap; 9 | import org.springframework.validation.BindingResult; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.ModelAttribute; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.ResponseBody; 17 | import org.springframework.web.servlet.ModelAndView; 18 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 19 | 20 | import simpleblog.domain.BlogService; 21 | import simpleblog.domain.EntryDoesNotExistException; 22 | import simpleblog.domain.Post; 23 | 24 | @Controller 25 | public class PostController { 26 | 27 | @Autowired 28 | BlogService blogService; 29 | 30 | @ModelAttribute("posts") 31 | public Iterable posts() { 32 | return blogService.getItems(); 33 | } 34 | 35 | @RequestMapping("/") 36 | public String newPost(ModelMap model) { 37 | model.put("post", new Post()); 38 | return "posts"; 39 | } 40 | 41 | @RequestMapping("/{id}/edit") 42 | public String editPost(@PathVariable Long id, ModelMap model) { 43 | model.put("post", blogService.findPostById(id)); 44 | return "posts"; 45 | } 46 | 47 | @RequestMapping("/{id}/delete") 48 | public String deletePost(@PathVariable Long id) { 49 | blogService.deletePostById(id); 50 | return "redirect:/"; 51 | } 52 | 53 | @RequestMapping(value = "/", method = RequestMethod.POST) 54 | public String save(@Valid Post post, BindingResult result, RedirectAttributes redirect) { 55 | if(result.hasErrors()) { 56 | return "posts"; 57 | } 58 | blogService.save(post); 59 | redirect.addFlashAttribute("msg", "Successfull save post entry!"); 60 | return "redirect:/"; 61 | } 62 | 63 | @RequestMapping(value = "/posts/error", method = RequestMethod.GET) 64 | public String error(@RequestParam String msg, ModelMap model, RedirectAttributes redirect) { 65 | redirect.addFlashAttribute("error", msg); 66 | return "redirect:/"; 67 | } 68 | 69 | @ExceptionHandler(ObjectOptimisticLockingFailureException.class) 70 | public ModelAndView optimisticLockingFailure() { 71 | ModelMap model = new ModelMap(); 72 | model.put("msg", "Optimistic Locking Failure!"); 73 | return new ModelAndView("redirect:/posts/error", model); 74 | } 75 | 76 | @ExceptionHandler(EntryDoesNotExistException.class) 77 | public ModelAndView postDoesNotExist(EntryDoesNotExistException exp) { 78 | ModelMap model = new ModelMap(); 79 | model.put("msg", String.format("Post entry with id %s no longer exists!", exp.getEntryId())); 80 | return new ModelAndView("redirect:/posts/error", model); 81 | } 82 | 83 | @RequestMapping("/ping") 84 | public @ResponseBody String ping() { 85 | return "OK"; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/BlogService.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | @Service 8 | public class BlogService { 9 | 10 | @Autowired 11 | PostRepository postRepository; 12 | 13 | @Transactional(readOnly = true) 14 | public Iterable getItems() { 15 | return postRepository.findAll(); 16 | } 17 | 18 | @Transactional 19 | public void save(Post post) { 20 | postRepository.save(post); 21 | } 22 | 23 | @Transactional(readOnly = true) 24 | public Post findPostById(Long id) { 25 | Post post = postRepository.findOne(id); 26 | if (post == null) { 27 | throw new EntryDoesNotExistException(id); 28 | } 29 | return post; 30 | } 31 | 32 | @Transactional 33 | public void deletePostById(Long id) { 34 | Post post = postRepository.findOne(id); 35 | if (post == null) { 36 | throw new EntryDoesNotExistException(id); 37 | } 38 | postRepository.delete(post); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/EntryDoesNotExistException.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | public class EntryDoesNotExistException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | private final Long entryId; 8 | 9 | public EntryDoesNotExistException(Long entryId) { 10 | this.entryId = entryId; 11 | } 12 | 13 | public Long getEntryId() { 14 | return entryId; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/Post.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Version; 10 | 11 | import org.hibernate.validator.constraints.NotEmpty; 12 | 13 | @Entity 14 | public class Post implements Serializable { 15 | 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @GeneratedValue 20 | private Long id; 21 | 22 | @Column(nullable = false) 23 | @NotEmpty 24 | @Title 25 | private String title; 26 | 27 | @Column(nullable = false) 28 | @NotEmpty 29 | private String description; 30 | 31 | @Version 32 | private Long version; 33 | 34 | public String getTitle() { 35 | return title; 36 | } 37 | 38 | public void setTitle(String title) { 39 | this.title = title; 40 | } 41 | 42 | public String getDescription() { 43 | return description; 44 | } 45 | 46 | public void setDescription(String description) { 47 | this.description = description; 48 | } 49 | 50 | public Long getId() { 51 | return id; 52 | } 53 | 54 | public void setId(Long id) { 55 | this.id = id; 56 | } 57 | 58 | public Long getVersion() { 59 | return version; 60 | } 61 | 62 | public void setVersion(Long version) { 63 | this.version = version; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/PostRepository.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface PostRepository extends CrudRepository { 6 | } -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/PostTitleValidator.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | import javax.validation.ConstraintValidator; 4 | import javax.validation.ConstraintValidatorContext; 5 | 6 | import org.apache.commons.lang.StringUtils; 7 | 8 | public class PostTitleValidator implements ConstraintValidator { 9 | 10 | @Override 11 | public void initialize(Title constraintAnnotation) { 12 | } 13 | 14 | @Override 15 | public boolean isValid(String value, ConstraintValidatorContext context) { 16 | if (StringUtils.equals(value, "test")) { 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/simpleblog/domain/Title.java: -------------------------------------------------------------------------------- 1 | package simpleblog.domain; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import javax.validation.Constraint; 8 | import javax.validation.Payload; 9 | 10 | @Retention(RUNTIME) 11 | @Constraint(validatedBy = PostTitleValidator.class) 12 | public @interface Title { 13 | String message() default "{simpleblog.model.Post.title}"; 14 | Class[] groups() default {}; 15 | Class[] payload() default {}; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/simpleblog/configuration/security-config.properties: -------------------------------------------------------------------------------- 1 | default.username=spring 2 | default.password=boot -------------------------------------------------------------------------------- /src/main/resources/simpleblog/configuration/webmvc-config.properties: -------------------------------------------------------------------------------- 1 | spring.thymeleaf.prefix=classpath:/simpleblog/view/ -------------------------------------------------------------------------------- /src/main/resources/simpleblog/view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple Blog 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/simpleblog/view/login.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 |
8 |

Login

9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/simpleblog/view/posts.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 | logout 8 |
9 |
10 |

msg

11 |
12 |
13 |

error

14 |
15 |
16 |

Post

17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 |

Posts

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 |
TitelDescription
titledescription 46 | 47 |
51 |
52 | 53 | -------------------------------------------------------------------------------- /src/main/resources/static/css/simpleblog.css: -------------------------------------------------------------------------------- 1 | div.msg { 2 | margin-top: 10px; 3 | margin-bottom: 0px; 4 | } 5 | 6 | div.error { 7 | margin-top: 10px; 8 | margin-bottom: 0px; 9 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/theme.bootstrap.css: -------------------------------------------------------------------------------- 1 | /************* 2 | Bootstrap theme 3 | *************/ 4 | /* jQuery Bootstrap Theme */ 5 | .tablesorter-bootstrap { 6 | width: 100%; 7 | } 8 | 9 | .tablesorter-bootstrap .tablesorter-header, 10 | .tablesorter-bootstrap tfoot th, 11 | .tablesorter-bootstrap tfoot td { 12 | font: bold 14px/20px Arial, Sans-serif; 13 | padding: 4px; 14 | margin: 0 0 18px; 15 | background-color: #eee; 16 | } 17 | 18 | .tablesorter-bootstrap .tablesorter-header { 19 | cursor: pointer; 20 | } 21 | 22 | .tablesorter-bootstrap .tablesorter-header-inner { 23 | position: relative; 24 | padding: 4px 18px 4px 4px; 25 | } 26 | 27 | /* bootstrap uses for icons */ 28 | .tablesorter-bootstrap .tablesorter-header i { 29 | font-size: 11px; 30 | position: absolute; 31 | right: 2px; 32 | top: 50%; 33 | margin-top: -7px; /* half the icon height; older IE doesn't like this */ 34 | width: 14px; 35 | height: 14px; 36 | background-repeat: no-repeat; 37 | line-height: 14px; 38 | display: inline-block; 39 | } 40 | 41 | .tablesorter-bootstrap .bootstrap-icon-unsorted { 42 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWVJREFUeNqUUL9Lw2AUTGP8mqGlpBQkNeCSRcckEBcHq1jImMElToKuDvpHFMGhU0BQcHBwLji6CE1B4uB/INQsDi4d2jQ/fPeZxo764OV6915f7lLJ81xot9tCURXqdVEUr7IsO6ffH9Q5BlEUCaLwWxWqTcbYnaIoh0Dw4gAvcWlxq1qt9hqNxg6hUGAP+uIPUrGs0qXLer2+v/pTX6QpxLtkc2U2m53ACb8sSdIDXerSEms2m6+DweAICA4d89KGbduf9MpEVdXQ9/2LVqv1CASHjjn3iq/x1xKFfxQPqGnada1W86bT6SiO42OS3qk3KPStLMvbk8nkfjwen/LLuq6blFymMB0KdUPSGhAcOualjX6/f0bCiC7NaWGPQr0BwaFjzn0gYJqmLAiCA8/zni3LmhuGkQPBoWPOPwQeaPIqD4fDruu6L6Zp5kBw6IudchmdJAkLw3DXcZwnIPjy/FuAAQCiqqWWCAFKcwAAAABJRU5ErkJggg==); 43 | } 44 | 45 | /* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ 46 | .tablesorter-bootstrap tr.odd td { 47 | background-color: #f9f9f9; 48 | } 49 | 50 | .tablesorter-bootstrap tbody > .odd:hover > td, 51 | .tablesorter-bootstrap tbody > .even:hover > td { 52 | background-color: #f5f5f5; 53 | } 54 | 55 | .tablesorter-bootstrap tr.even td { 56 | background-color: #fff; 57 | } 58 | 59 | /* processing icon */ 60 | .tablesorter-bootstrap .tablesorter-processing { 61 | background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); 62 | background-position: center center !important; 63 | background-repeat: no-repeat !important; 64 | position: absolute; 65 | z-index: 1000; 66 | } 67 | 68 | /* caption */ 69 | .caption { 70 | background: #fff; 71 | } 72 | 73 | /* filter widget */ 74 | .tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter { 75 | width: 98%; 76 | height: auto; 77 | margin: 0 auto; 78 | padding: 4px 6px; 79 | color: #333; 80 | -webkit-box-sizing: border-box; 81 | -moz-box-sizing: border-box; 82 | box-sizing: border-box; 83 | -webkit-transition: height 0.1s ease; 84 | -moz-transition: height 0.1s ease; 85 | -o-transition: height 0.1s ease; 86 | transition: height 0.1s ease; 87 | } 88 | 89 | .tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled { 90 | background-color: #eee; 91 | color: #555; 92 | cursor: not-allowed; 93 | border: 1px solid #ccc; 94 | border-radius: 4px; 95 | box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset; 96 | box-sizing: border-box; 97 | transition: height 0.1s ease; 98 | } 99 | 100 | .tablesorter-bootstrap .tablesorter-filter-row td { 101 | background: #efefef; 102 | line-height: normal; 103 | text-align: center; 104 | padding: 4px 6px; 105 | vertical-align: middle; 106 | -webkit-transition: line-height 0.1s ease; 107 | -moz-transition: line-height 0.1s ease; 108 | -o-transition: line-height 0.1s ease; 109 | transition: line-height 0.1s ease; 110 | } 111 | 112 | /* hidden filter row */ 113 | .tablesorter-bootstrap .tablesorter-filter-row.hideme td { 114 | padding: 2px; /* change this to modify the thickness of the closed border row */ 115 | margin: 0; 116 | line-height: 0; 117 | } 118 | 119 | .tablesorter-bootstrap .tablesorter-filter-row.hideme .tablesorter-filter { 120 | height: 1px; 121 | min-height: 0; 122 | border: 0; 123 | padding: 0; 124 | margin: 0; 125 | /* don't use visibility: hidden because it disables tabbing */ 126 | opacity: 0; 127 | filter: alpha(opacity=0); 128 | } 129 | 130 | /* pager plugin */ 131 | .tablesorter-bootstrap .tablesorter-pager select { 132 | padding: 4px 6px; 133 | } 134 | 135 | .tablesorter-bootstrap .tablesorter-pager .pagedisplay { 136 | border: 0; 137 | } 138 | 139 | /* tfoot i for pager controls */ 140 | .tablesorter-bootstrap tfoot i { 141 | font-size: 11px; 142 | } 143 | 144 | /* ajax error row */ 145 | .tablesorter .tablesorter-errorRow td { 146 | text-align: center; 147 | cursor: pointer; 148 | background-color: #e6bf99; 149 | } 150 | 151 | -------------------------------------------------------------------------------- /src/main/resources/static/js/jquery.tablesorter.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * TableSorter 2.15.5 min - Client-side table sorting with ease! 3 | * Copyright (c) 2007 Christian Bach 4 | */ 5 | !function(g){g.extend({tablesorter:new function(){function d(){var a=arguments[0],b=1':"";l.$headers=g(a).find(l.selectorHeaders).each(function(a){h=g(this);c=l.headers[a];l.headerContent[a]=g(this).html();k=l.headerTemplate.replace(/\{content\}/g,g(this).html()).replace(/\{icon\}/g,w);l.onRenderTemplate&&(e=l.onRenderTemplate.apply(h,[a,k]))&&"string"===typeof e&&(k=e);g(this).html('
'+k+"
");l.onRenderHeader&&l.onRenderHeader.apply(h,[a]);this.column= b[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=C(f.getData(h,c,"sortInitialOrder")||l.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;this.lockedOrder=!1;n=f.getData(h,c,"lockedOrder")||!1;"undefined"!==typeof n&&!1!==n&&(this.order=this.lockedOrder=C(n)?[1,1,1]:[0,0,0]);h.addClass(f.css.header+" "+l.cssHeader);l.headerList[a]=this;h.parent().addClass(f.css.headerRow+" "+l.cssHeaderRow).attr("role","row");l.tabIndex&&h.attr("tabindex",0)}).attr({scope:"col",role:"columnheader"});G(a);l.debug&& (u("Built headers:",q),d(l.$headers))}function B(a,b,c){var h=a.config;h.$table.find(h.selectorRemove).remove();s(a);v(a);H(h.$table,b,c)}function G(a){var b,c,h=a.config;h.$headers.each(function(e,d){c=g(d);b="false"===f.getData(d,h.headers[e],"sorter");d.sortDisabled=b;c[b?"addClass":"removeClass"]("sorter-false").attr("aria-disabled",""+b);a.id&&(b?c.removeAttr("aria-controls"):c.attr("aria-controls",a.id))})}function F(a){var b,c,h,e=a.config,d=e.sortList,k=f.css.sortNone+" "+e.cssNone,n=[f.css.sortAsc+ " "+e.cssAsc,f.css.sortDesc+" "+e.cssDesc],q=["ascending","descending"],l=g(a).find("tfoot tr").children().removeClass(n.join(" "));e.$headers.removeClass(n.join(" ")).addClass(k).attr("aria-sort","none");h=d.length;for(b=0;b"),c=g(a).width();g(a.tBodies[0]).find("tr:first").children("td:visible").each(function(){b.append(g("").css("width", parseInt(g(this).width()/c*1E3,10)/10+"%"))});g(a).prepend(b)}}function M(a,b){var c,h,e,f=a.config,d=b||f.sortList;f.sortList=[];g.each(d,function(a,b){c=[parseInt(b[0],10),parseInt(b[1],10)];if(e=f.$headers[c[0]])f.sortList.push(c),h=g.inArray(c[1],e.order),e.count=0<=h?h:c[1]%(f.sortReset?3:2)})}function N(a,b){return a&&a[b]?a[b].type||"":""}function O(a,b,c){var h,e,d,k=a.config,n=!c[k.sortMultiSortKey],q=g(a);q.trigger("sortStart",a);b.count=c[k.sortResetKey]?2:(b.count+1)%(k.sortReset?3:2); k.sortRestart&&(e=b,k.$headers.each(function(){this===e||!n&&g(this).is("."+f.css.sortDesc+",."+f.css.sortAsc)||(this.count=-1)}));e=b.column;if(n){k.sortList=[];if(null!==k.sortForce)for(h=k.sortForce,c=0;ch&&(k.sortList.push([e,h]),1h&&(k.sortList.push([e,h]),1 thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me",debug:!1,headerList:[],empties:{},strings:{},parsers:[]};f.css={table:"tablesorter",childRow:"tablesorter-childRow",header:"tablesorter-header",headerRow:"tablesorter-headerRow",headerIn:"tablesorter-header-inner",icon:"tablesorter-icon",info:"tablesorter-infoOnly",processing:"tablesorter-processing", sortAsc:"tablesorter-headerAsc",sortDesc:"tablesorter-headerDesc",sortNone:"tablesorter-headerUnSorted"};f.language={sortAsc:"Ascending sort applied, ",sortDesc:"Descending sort applied, ",sortNone:"No sort applied, ",nextAsc:"activate to apply an ascending sort",nextDesc:"activate to apply a descending sort",nextNone:"activate to remove the sort"};f.log=d;f.benchmark=u;f.construct=function(a){return this.each(function(){var b=g.extend(!0,{},f.defaults,a);!this.hasInitialized&&f.buildTable&&"TABLE"!== this.tagName&&f.buildTable(this,b);f.setup(this,b)})};f.setup=function(a,b){if(!a||!a.tHead||0===a.tBodies.length||!0===a.hasInitialized)return b.debug?d("ERROR: stopping initialization! No table, thead, tbody or tablesorter has already been initialized"):"";var c="",h=g(a),e=g.metadata;a.hasInitialized=!1;a.isProcessing=!0;a.config=b;g.data(a,"tablesorter",b);b.debug&&g.data(a,"startoveralltimer",new Date);b.supportsTextContent="x"===g("x")[0].textContent;b.supportsDataObject=function(a){a[0]= parseInt(a[0],10);return 1'),c=g.fn.detach?b.detach():b.remove();c=g(a).find("span.tablesorter-savemyplace");b.insertAfter(c);c.remove();a.isProcessing=!1};f.clearTableBody=function(a){g(a)[0].config.$tbodies.empty()};f.bindEvents=function(a,b){a=g(a)[0];var c,h=a.config;b.find(h.selectorSort).add(b.filter(h.selectorSort)).unbind("mousedown.tablesorter mouseup.tablesorter sort.tablesorter keyup.tablesorter").bind("mousedown.tablesorter mouseup.tablesorter sort.tablesorter keyup.tablesorter", function(e,d){var f;f=e.type;if(!(1!==(e.which||e.button)&&!/sort|keyup/.test(f)||"keyup"===f&&13!==e.which||"mouseup"===f&&!0!==d&&250<(new Date).getTime()-c)){if("mousedown"===f)return c=(new Date).getTime(),"INPUT"===e.target.tagName?"":!h.cancelSelection;h.delayInit&&m(h.cache)&&v(a);f=/TH|TD/.test(this.tagName)?this:g(this).parents("th, td")[0];f=h.$headers[b.index(f)];f.sortDisabled||O(a,f,e)}});h.cancelSelection&&b.attr("unselectable","on").bind("selectstart",!1).css({"user-select":"none", MozUserSelect:"none"})};f.restoreHeaders=function(a){var b=g(a)[0].config;b.$table.find(b.selectorHeaders).each(function(a){g(this).find("."+f.css.headerIn).length&&g(this).html(b.headerContent[a])})};f.destroy=function(a,b,c){a=g(a)[0];if(a.hasInitialized){f.refreshWidgets(a,!0,!0);var h=g(a),e=a.config,d=h.find("thead:first"),k=d.find("tr."+f.css.headerRow).removeClass(f.css.headerRow+" "+e.cssHeaderRow),n=h.find("tfoot:first > tr").children("th, td");d.find("tr").not(k).remove();h.removeData("tablesorter").unbind("sortReset update updateAll updateRows updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd ".split(" ").join(".tablesorter ")); e.$headers.add(n).removeClass([f.css.header,e.cssHeader,e.cssAsc,e.cssDesc,f.css.sortAsc,f.css.sortDesc,f.css.sortNone].join(" ")).removeAttr("data-column");k.find(e.selectorSort).unbind("mousedown.tablesorter mouseup.tablesorter keypress.tablesorter");f.restoreHeaders(a);!1!==b&&h.removeClass(f.css.table+" "+e.tableClass+" tablesorter-"+e.theme);a.hasInitialized=!1;"function"===typeof c&&c(a)}};f.regex={chunk:/(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,hex:/^0x[0-9a-f]+$/i}; f.sortNatural=function(a,b){if(a===b)return 0;var c,h,e,d,g,n;h=f.regex;if(h.hex.test(b)){c=parseInt(a.match(h.hex),16);e=parseInt(b.match(h.hex),16);if(ce)return 1}c=a.replace(h.chunk,"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");h=b.replace(h.chunk,"\\0$1\\0").replace(/\\0$/,"").replace(/^\\0/,"").split("\\0");n=Math.max(c.length,h.length);for(g=0;gd)return 1}return 0};f.sortNaturalAsc=function(a,b,c,d,e){if(a===b)return 0;c=e.string[e.empties[c]||e.emptyTo];return""===a&&0!==c?"boolean"===typeof c?c?-1:1:-c||-1:""===b&&0!==c?"boolean"===typeof c?c?1:-1:c||1:f.sortNatural(a,b)};f.sortNaturalDesc=function(a,b,c,d,e){if(a===b)return 0;c=e.string[e.empties[c]||e.emptyTo];return""===a&&0!==c?"boolean"===typeof c?c?-1:1:c||1:""===b&&0!==c?"boolean"===typeof c?c?1:-1:-c||-1:f.sortNatural(b, a)};f.sortText=function(a,b){return a>b?1:ag.inArray(k[h].id,m))&&(e.debug&&d('Refeshing widgets: Removing "'+k[h].id+'"'),k[h].hasOwnProperty("remove")&&e.widgetInit[k[h].id]&&(k[h].remove(a,e,e.widgetOptions),e.widgetInit[k[h].id]=!1));!0!==c&&f.applyWidget(a,b)};f.getData=function(a,b,c){var d="";a=g(a);var e,f;if(!a.length)return"";e=g.metadata?a.metadata():!1;f=" "+(a.attr("class")||"");"undefined"!==typeof a.data(c)||"undefined"!==typeof a.data(c.toLowerCase())? d+=a.data(c)||a.data(c.toLowerCase()):e&&"undefined"!==typeof e[c]?d+=e[c]:b&&"undefined"!==typeof b[c]?d+=b[c]:" "!==f&&f.match(" "+c+"-")&&(d=f.match(RegExp("\\s"+c+"-([\\w-]+)"))[1]||"");return g.trim(d)};f.formatFloat=function(a,b){if("string"!==typeof a||""===a)return a;var c;a=(b&&b.config?!1!==b.config.usNumberFormat:"undefined"!==typeof b?b:1)?a.replace(/,/g,""):a.replace(/[\s|\.]/g,"").replace(/,/g,".");/^\s*\([.\d]+\)/.test(a)&&(a=a.replace(/^\s*\(([.\d]+)\)/,"-$1"));c=parseFloat(a);return isNaN(c)? g.trim(a):c};f.isDigit=function(a){return isNaN(a)?/^[\-+(]?\d+[)]?$/.test(a.toString().replace(/[,.'"\s]/g,"")):!0}}});var p=g.tablesorter;g.fn.extend({tablesorter:p.construct});p.addParser({id:"text",is:function(){return!0},format:function(d,u){var m=u.config;d&&(d=g.trim(m.ignoreCase?d.toLocaleLowerCase():d),d=m.sortLocaleCompare?p.replaceAccents(d):d);return d},type:"text"});p.addParser({id:"digit",is:function(d){return p.isDigit(d)},format:function(d,u){var m=p.formatFloat((d||"").replace(/[^\w,. \-()]/g, ""),u);return d&&"number"===typeof m?m:d?g.trim(d&&u.config.ignoreCase?d.toLocaleLowerCase():d):d},type:"numeric"});p.addParser({id:"currency",is:function(d){return/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/.test((d||"").replace(/[+\-,. ]/g,""))},format:function(d,u){var m=p.formatFloat((d||"").replace(/[^\w,. \-()]/g,""),u);return d&&"number"===typeof m?m:d?g.trim(d&&u.config.ignoreCase?d.toLocaleLowerCase():d):d},type:"numeric"});p.addParser({id:"ipAddress", is:function(d){return/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/.test(d)},format:function(d,g){var m,t=d?d.split("."):"",s="",v=t.length;for(m=0;md.length},format:function(d,g){return d?p.formatFloat(d.replace(/%/g,""),g):d},type:"numeric"});p.addParser({id:"usLongDate",is:function(d){return/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i.test(d)||/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i.test(d)},format:function(d,g){return d?p.formatFloat((new Date(d.replace(/(\S)([AP]M)$/i, "$1 $2"))).getTime()||"",g):d},type:"numeric"});p.addParser({id:"shortDate",is:function(d){return/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/.test((d||"").replace(/\s+/g," ").replace(/[\-.,]/g,"/"))},format:function(d,g,m,t){if(d){m=g.config;var s=m.$headers.filter("[data-column="+t+"]:last");t=s.length&&s[0].dateFormat||p.getData(s,m.headers[t],"dateFormat")||m.dateFormat;d=d.replace(/\s+/g," ").replace(/[\-.,]/g,"/");"mmddyyyy"===t?d=d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"):"ddmmyyyy"===t?d=d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/,"$3/$2/$1"):"yyyymmdd"===t&&(d=d.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/,"$1/$2/$3"))}return d?p.formatFloat((new Date(d)).getTime()||"",g):d},type:"numeric"});p.addParser({id:"time",is:function(d){return/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i.test(d)},format:function(d,g){return d?p.formatFloat((new Date("2000/01/01 "+d.replace(/(\S)([AP]M)$/i,"$1 $2"))).getTime()||"",g):d},type:"numeric"});p.addParser({id:"metadata", is:function(){return!1},format:function(d,p,m){d=p.config;d=d.parserMetadataName?d.parserMetadataName:"sortValue";return g(m).metadata()[d]},type:"numeric"});p.addWidget({id:"zebra",priority:90,format:function(d,u,m){var t,s,v,A,D,C,E=RegExp(u.cssChildRow,"i"),B=u.$tbodies;u.debug&&(D=new Date);for(d=0;d in the header 25 | sortNone : 'bootstrap-icon-unsorted', 26 | sortAsc : 'icon-chevron-up glyphicon glyphicon-chevron-up', 27 | sortDesc : 'icon-chevron-down glyphicon glyphicon-chevron-down', 28 | active : '', // applied when column is sorted 29 | hover : '', // use custom css here - bootstrap class may not override it 30 | filterRow : '', // filter row class 31 | even : '', // even row zebra striping 32 | odd : '' // odd row zebra striping 33 | }, 34 | "jui" : { 35 | table : 'ui-widget ui-widget-content ui-corner-all', // table classes 36 | caption : 'ui-widget-content ui-corner-all', 37 | header : 'ui-widget-header ui-corner-all ui-state-default', // header classes 38 | footerRow : '', 39 | footerCells: '', 40 | icons : 'ui-icon', // icon class added to the in the header 41 | sortNone : 'ui-icon-carat-2-n-s', 42 | sortAsc : 'ui-icon-carat-1-n', 43 | sortDesc : 'ui-icon-carat-1-s', 44 | active : 'ui-state-active', // applied when column is sorted 45 | hover : 'ui-state-hover', // hover class 46 | filterRow : '', 47 | even : 'ui-widget-content', // even row zebra striping 48 | odd : 'ui-state-default' // odd row zebra striping 49 | } 50 | }; 51 | 52 | $.extend(ts.css, { 53 | filterRow : 'tablesorter-filter-row', // filter 54 | filter : 'tablesorter-filter', 55 | wrapper : 'tablesorter-wrapper', // ui theme & resizable 56 | resizer : 'tablesorter-resizer', // resizable 57 | grip : 'tablesorter-resizer-grip', 58 | sticky : 'tablesorter-stickyHeader', // stickyHeader 59 | stickyVis : 'tablesorter-sticky-visible' 60 | }); 61 | 62 | // *** Store data in local storage, with a cookie fallback *** 63 | /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) 64 | if you need it, then include https://github.com/douglascrockford/JSON-js 65 | 66 | $.parseJSON is not available is jQuery versions older than 1.4.1, using older 67 | versions will only allow storing information for one page at a time 68 | 69 | // *** Save data (JSON format only) *** 70 | // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid 71 | var val = { "mywidget" : "data1" }; // valid JSON uses double quotes 72 | // $.tablesorter.storage(table, key, val); 73 | $.tablesorter.storage(table, 'tablesorter-mywidget', val); 74 | 75 | // *** Get data: $.tablesorter.storage(table, key); *** 76 | v = $.tablesorter.storage(table, 'tablesorter-mywidget'); 77 | // val may be empty, so also check for your data 78 | val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; 79 | alert(val); // "data1" if saved, or "" if not 80 | */ 81 | ts.storage = function(table, key, value, options) { 82 | table = $(table)[0]; 83 | var cookieIndex, cookies, date, 84 | hasLocalStorage = false, 85 | values = {}, 86 | c = table.config, 87 | $table = $(table), 88 | id = options && options.id || $table.attr(options && options.group || 89 | 'data-table-group') || table.id || $('.tablesorter').index( $table ), 90 | url = options && options.url || $table.attr(options && options.page || 91 | 'data-table-page') || c && c.fixedUrl || window.location.pathname; 92 | // https://gist.github.com/paulirish/5558557 93 | if ("localStorage" in window) { 94 | try { 95 | window.localStorage.setItem('_tmptest', 'temp'); 96 | hasLocalStorage = true; 97 | window.localStorage.removeItem('_tmptest'); 98 | } catch(error) {} 99 | } 100 | // *** get value *** 101 | if ($.parseJSON) { 102 | if (hasLocalStorage) { 103 | values = $.parseJSON(localStorage[key] || '{}'); 104 | } else { 105 | // old browser, using cookies 106 | cookies = document.cookie.split(/[;\s|=]/); 107 | // add one to get from the key to the value 108 | cookieIndex = $.inArray(key, cookies) + 1; 109 | values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || '{}') : {}; 110 | } 111 | } 112 | // allow value to be an empty string too 113 | if ((value || value === '') && window.JSON && JSON.hasOwnProperty('stringify')) { 114 | // add unique identifiers = url pathname > table ID/index on page > data 115 | if (!values[url]) { 116 | values[url] = {}; 117 | } 118 | values[url][id] = value; 119 | // *** set value *** 120 | if (hasLocalStorage) { 121 | localStorage[key] = JSON.stringify(values); 122 | } else { 123 | date = new Date(); 124 | date.setTime(date.getTime() + (31536e+6)); // 365 days 125 | document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g,'\"') + '; expires=' + date.toGMTString() + '; path=/'; 126 | } 127 | } else { 128 | return values && values[url] ? values[url][id] : {}; 129 | } 130 | }; 131 | 132 | // Add a resize event to table headers 133 | // ************************** 134 | ts.addHeaderResizeEvent = function(table, disable, settings) { 135 | var headers, 136 | defaults = { 137 | timer : 250 138 | }, 139 | options = $.extend({}, defaults, settings), 140 | c = table.config, 141 | wo = c.widgetOptions, 142 | checkSizes = function(triggerEvent) { 143 | wo.resize_flag = true; 144 | headers = []; 145 | c.$headers.each(function() { 146 | var $header = $(this), 147 | sizes = $header.data('savedSizes') || [0,0], // fixes #394 148 | width = this.offsetWidth, 149 | height = this.offsetHeight; 150 | if (width !== sizes[0] || height !== sizes[1]) { 151 | $header.data('savedSizes', [ width, height ]); 152 | headers.push(this); 153 | } 154 | }); 155 | if (headers.length && triggerEvent !== false) { 156 | c.$table.trigger('resize', [ headers ]); 157 | } 158 | wo.resize_flag = false; 159 | }; 160 | checkSizes(false); 161 | clearInterval(wo.resize_timer); 162 | if (disable) { 163 | wo.resize_flag = false; 164 | return false; 165 | } 166 | wo.resize_timer = setInterval(function() { 167 | if (wo.resize_flag) { return; } 168 | checkSizes(); 169 | }, options.timer); 170 | }; 171 | 172 | // Widget: General UI theme 173 | // "uitheme" option in "widgetOptions" 174 | // ************************** 175 | ts.addWidget({ 176 | id: "uitheme", 177 | priority: 10, 178 | format: function(table, c, wo) { 179 | var time, classes, $header, $icon, $tfoot, 180 | themesAll = ts.themes, 181 | $table = c.$table, 182 | $headers = c.$headers, 183 | theme = c.theme || 'jui', 184 | themes = themesAll[theme] || themesAll.jui, 185 | remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc; 186 | if (c.debug) { time = new Date(); } 187 | // initialization code - run once 188 | if (!$table.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized) { 189 | // update zebra stripes 190 | if (themes.even !== '') { wo.zebra[0] += ' ' + themes.even; } 191 | if (themes.odd !== '') { wo.zebra[1] += ' ' + themes.odd; } 192 | // add caption style 193 | $table.find('caption').addClass(themes.caption); 194 | // add table/footer class names 195 | $tfoot = $table 196 | // remove other selected themes 197 | .removeClass( c.theme === '' ? '' : 'tablesorter-' + c.theme ) 198 | .addClass('tablesorter-' + theme + ' ' + themes.table) // add theme widget class name 199 | .find('tfoot'); 200 | if ($tfoot.length) { 201 | $tfoot 202 | .find('tr').addClass(themes.footerRow) 203 | .children('th, td').addClass(themes.footerCells); 204 | } 205 | // update header classes 206 | $headers 207 | .addClass(themes.header) 208 | .not('.sorter-false') 209 | .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { 210 | // toggleClass with switch added in jQuery 1.3 211 | $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover); 212 | }); 213 | if (!$headers.find('.' + ts.css.wrapper).length) { 214 | // Firefox needs this inner div to position the resizer correctly 215 | $headers.wrapInner('
'); 216 | } 217 | if (c.cssIcon) { 218 | // if c.cssIcon is '', then no is added to the header 219 | $headers.find('.' + ts.css.icon).addClass(themes.icons); 220 | } 221 | if ($table.hasClass('hasFilters')) { 222 | $headers.find('.' + ts.css.filterRow).addClass(themes.filterRow); 223 | } 224 | } 225 | $.each($headers, function() { 226 | $header = $(this); 227 | $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $header; 228 | if (this.sortDisabled) { 229 | // no sort arrows for disabled columns! 230 | $header.removeClass(remove); 231 | $icon.removeClass(remove + ' ' + themes.icons); 232 | } else { 233 | classes = ($header.hasClass(ts.css.sortAsc)) ? 234 | themes.sortAsc : 235 | ($header.hasClass(ts.css.sortDesc)) ? themes.sortDesc : 236 | $header.hasClass(ts.css.header) ? themes.sortNone : ''; 237 | $header[classes === themes.sortNone ? 'removeClass' : 'addClass'](themes.active); 238 | $icon.removeClass(remove).addClass(classes); 239 | } 240 | }); 241 | if (c.debug) { 242 | ts.benchmark("Applying " + theme + " theme", time); 243 | } 244 | }, 245 | remove: function(table, c, wo) { 246 | var $table = c.$table, 247 | theme = c.theme || 'jui', 248 | themes = ts.themes[ theme ] || ts.themes.jui, 249 | $headers = $table.children('thead').children(), 250 | remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc; 251 | $table 252 | .removeClass('tablesorter-' + theme + ' ' + themes.table) 253 | .find(ts.css.header).removeClass(themes.header); 254 | $headers 255 | .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover 256 | .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) 257 | .find('.' + ts.css.filterRow) 258 | .removeClass(themes.filterRow); 259 | $headers.find('.' + ts.css.icon).removeClass(themes.icons); 260 | } 261 | }); 262 | 263 | // Widget: Column styles 264 | // "columns", "columns_thead" (true) and 265 | // "columns_tfoot" (true) options in "widgetOptions" 266 | // ************************** 267 | ts.addWidget({ 268 | id: "columns", 269 | priority: 30, 270 | options : { 271 | columns : [ "primary", "secondary", "tertiary" ] 272 | }, 273 | format: function(table, c, wo) { 274 | var time, $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, 275 | $table = c.$table, 276 | $tbodies = c.$tbodies, 277 | sortList = c.sortList, 278 | len = sortList.length, 279 | // removed c.widgetColumns support 280 | css = wo && wo.columns || [ "primary", "secondary", "tertiary" ], 281 | last = css.length - 1; 282 | remove = css.join(' '); 283 | if (c.debug) { 284 | time = new Date(); 285 | } 286 | // check if there is a sort (on initialization there may not be one) 287 | for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 288 | $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody 289 | $rows = $tbody.children('tr'); 290 | // loop through the visible rows 291 | $rows.each(function() { 292 | $row = $(this); 293 | if (this.style.display !== 'none') { 294 | // remove all columns class names 295 | $cells = $row.children().removeClass(remove); 296 | // add appropriate column class names 297 | if (sortList && sortList[0]) { 298 | // primary sort column class 299 | $cells.eq(sortList[0][0]).addClass(css[0]); 300 | if (len > 1) { 301 | for (indx = 1; indx < len; indx++) { 302 | // secondary, tertiary, etc sort column classes 303 | $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); 304 | } 305 | } 306 | } 307 | } 308 | }); 309 | ts.processTbody(table, $tbody, false); 310 | } 311 | // add classes to thead and tfoot 312 | rows = wo.columns_thead !== false ? ['thead tr'] : []; 313 | if (wo.columns_tfoot !== false) { 314 | rows.push('tfoot tr'); 315 | } 316 | if (rows.length) { 317 | $rows = $table.find( rows.join(',') ).children().removeClass(remove); 318 | if (len) { 319 | for (indx = 0; indx < len; indx++) { 320 | // add primary. secondary, tertiary, etc sort column classes 321 | $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); 322 | } 323 | } 324 | } 325 | if (c.debug) { 326 | ts.benchmark("Applying Columns widget", time); 327 | } 328 | }, 329 | remove: function(table, c, wo) { 330 | var tbodyIndex, $tbody, 331 | $tbodies = c.$tbodies, 332 | remove = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' '); 333 | c.$headers.removeClass(remove); 334 | c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); 335 | for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 336 | $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 337 | $tbody.children('tr').each(function() { 338 | $(this).children().removeClass(remove); 339 | }); 340 | ts.processTbody(table, $tbody, false); // restore tbody 341 | } 342 | } 343 | }); 344 | 345 | // Widget: filter 346 | // ************************** 347 | ts.addWidget({ 348 | id: "filter", 349 | priority: 50, 350 | options : { 351 | filter_childRows : false, // if true, filter includes child row content in the search 352 | filter_columnFilters : true, // if true, a filter will be added to the top of each table column 353 | filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added) 354 | filter_external : '', // jQuery selector string (or jQuery object) of external filters 355 | filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin 356 | filter_formatter : null, // add custom filter elements to the filter row 357 | filter_functions : null, // add custom filter functions using this option 358 | filter_hideEmpty : true, // hide filter row when table is empty 359 | filter_hideFilters : false, // collapse filter row when mouse leaves the area 360 | filter_ignoreCase : true, // if true, make all searches case-insensitive 361 | filter_liveSearch : true, // if true, search column content while the user types (with a delay) 362 | filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down 363 | filter_reset : null, // jQuery selector string of an element used to reset the filters 364 | filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters 365 | filter_searchDelay : 300, // typing delay in milliseconds before starting a search 366 | filter_startsWith : false, // if true, filter start from the beginning of the cell contents 367 | filter_useParsedData : false, // filter all data using parsed content 368 | filter_serversideFiltering : false, // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used. 369 | filter_defaultAttrib : 'data-value' // data attribute in the header cell that contains the default filter value 370 | }, 371 | format: function(table, c, wo) { 372 | if (!c.$table.hasClass('hasFilters')) { 373 | ts.filter.init(table, c, wo); 374 | } 375 | }, 376 | remove: function(table, c, wo) { 377 | var tbodyIndex, $tbody, 378 | $table = c.$table, 379 | $tbodies = c.$tbodies; 380 | $table 381 | .removeClass('hasFilters') 382 | // add .tsfilter namespace to all BUT search 383 | .unbind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter ')) 384 | .find('.' + ts.css.filterRow).remove(); 385 | for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 386 | $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 387 | $tbody.children().removeClass(wo.filter_filteredRow).show(); 388 | ts.processTbody(table, $tbody, false); // restore tbody 389 | } 390 | if (wo.filter_reset) { 391 | $(document).undelegate(wo.filter_reset, 'click.tsfilter'); 392 | } 393 | } 394 | }); 395 | 396 | ts.filter = { 397 | 398 | // regex used in filter "check" functions - not for general use and not documented 399 | regex: { 400 | regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex 401 | child : /tablesorter-childRow/, // child row class name; this gets updated in the script 402 | filtered : /filtered/, // filtered (hidden) row class name; updated in the script 403 | type : /undefined|number/, // check type 404 | exact : /(^[\"|\'|=]+)|([\"|\'|=]+$)/g, // exact match (allow '==') 405 | nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser) 406 | operators : /[<>=]/g // replace operators 407 | }, 408 | // function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) 409 | // filter = array of filter input values; iFilter = same array, except lowercase 410 | // exact = table cell text (or parsed data if column parser enabled) 411 | // iExact = same as exact, except lowercase 412 | // cached = table cell text from cache, so it has been parsed 413 | // index = column index; table = table element (DOM) 414 | // wo = widget options (table.config.widgetOptions) 415 | // parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class) 416 | types: { 417 | // Look for regex 418 | regex: function( filter, iFilter, exact, iExact ) { 419 | if ( ts.filter.regex.regex.test(iFilter) ) { 420 | var matches, 421 | regex = ts.filter.regex.regex.exec(iFilter); 422 | try { 423 | matches = new RegExp(regex[1], regex[2]).test( iExact ); 424 | } catch (error) { 425 | matches = false; 426 | } 427 | return matches; 428 | } 429 | return null; 430 | }, 431 | // Look for operators >, >=, < or <= 432 | operators: function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) { 433 | if ( /^[<>]=?/.test(iFilter) ) { 434 | var cachedValue, result, 435 | c = table.config, 436 | query = ts.formatFloat( iFilter.replace(ts.filter.regex.operators, ''), table ), 437 | parser = c.parsers[index], 438 | savedSearch = query; 439 | // parse filter value in case we're comparing numbers (dates) 440 | if (parsed[index] || parser.type === 'numeric') { 441 | cachedValue = parser.format( '' + iFilter.replace(ts.filter.regex.operators, ''), table, c.$headers.eq(index), index ); 442 | query = ( typeof query === "number" && cachedValue !== '' && !isNaN(cachedValue) ) ? cachedValue : query; 443 | } 444 | // iExact may be numeric - see issue #149; 445 | // check if cached is defined, because sometimes j goes out of range? (numeric columns) 446 | cachedValue = ( parsed[index] || parser.type === 'numeric' ) && !isNaN(query) && cached ? cached : 447 | isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) : 448 | ts.formatFloat( iExact, table ); 449 | if ( />/.test(iFilter) ) { result = />=/.test(iFilter) ? cachedValue >= query : cachedValue > query; } 450 | if ( /= 0 : fltr == iExact; 463 | } 464 | return null; 465 | }, 466 | // Look for a not match 467 | notMatch: function( filter, iFilter, exact, iExact, cached, index, table, wo ) { 468 | if ( /^\!/.test(iFilter) ) { 469 | iFilter = iFilter.replace('!', ''); 470 | var indx = iExact.search( $.trim(iFilter) ); 471 | return iFilter === '' ? true : !(wo.filter_startsWith ? indx === 0 : indx >= 0); 472 | } 473 | return null; 474 | }, 475 | // Look for an AND or && operator (logical and) 476 | and : function( filter, iFilter, exact, iExact ) { 477 | if ( /\s+(AND|&&)\s+/g.test(filter) ) { 478 | var query = iFilter.split( /(?:\s+(?:and|&&)\s+)/g ), 479 | result = iExact.search( $.trim(query[0]) ) >= 0, 480 | indx = query.length - 1; 481 | while (result && indx) { 482 | result = result && iExact.search( $.trim(query[indx]) ) >= 0; 483 | indx--; 484 | } 485 | return result; 486 | } 487 | return null; 488 | }, 489 | // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu! 490 | range : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) { 491 | if ( /\s+(-|to)\s+/.test(iFilter) ) { 492 | var result, tmp, 493 | c = table.config, 494 | query = iFilter.split(/(?: - | to )/), // make sure the dash is for a range and not indicating a negative number 495 | range1 = ts.formatFloat(query[0].replace(ts.filter.regex.nondigit, ''), table), 496 | range2 = ts.formatFloat(query[1].replace(ts.filter.regex.nondigit, ''), table); 497 | // parse filter value in case we're comparing numbers (dates) 498 | if (parsed[index] || c.parsers[index].type === 'numeric') { 499 | result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index); 500 | range1 = (result !== '' && !isNaN(result)) ? result : range1; 501 | result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index); 502 | range2 = (result !== '' && !isNaN(result)) ? result : range2; 503 | } 504 | result = ( parsed[index] || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? cached : 505 | isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) : 506 | ts.formatFloat( iExact, table ); 507 | if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap 508 | return (result >= range1 && result <= range2) || (range1 === '' || range2 === ''); 509 | } 510 | return null; 511 | }, 512 | // Look for wild card: ? = single, * = multiple, or | = logical OR 513 | wild : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed, rowArray ) { 514 | if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/i.test(filter) ) { 515 | var c = table.config, 516 | query = iFilter.replace(/\s+OR\s+/gi,"|"); 517 | // look for an exact match with the "or" unless the "filter-match" class is found 518 | if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) { 519 | query = $.isArray(rowArray) ? '(' + query + ')' : '^(' + query + ')$'; 520 | } 521 | return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(iExact); 522 | } 523 | return null; 524 | }, 525 | // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license) 526 | fuzzy: function( filter, iFilter, exact, iExact ) { 527 | if ( /^~/.test(iFilter) ) { 528 | var indx, 529 | patternIndx = 0, 530 | len = iExact.length, 531 | pattern = iFilter.slice(1); 532 | for (indx = 0; indx < len; indx++) { 533 | if (iExact[indx] === pattern[patternIndx]) { 534 | patternIndx += 1; 535 | } 536 | } 537 | if (patternIndx === pattern.length) { 538 | return true; 539 | } 540 | return false; 541 | } 542 | return null; 543 | } 544 | }, 545 | init: function(table, c, wo) { 546 | var options, string, $header, column, filters, time; 547 | if (c.debug) { 548 | time = new Date(); 549 | } 550 | c.$table.addClass('hasFilters'); 551 | 552 | ts.filter.regex.child = new RegExp(c.cssChildRow); 553 | ts.filter.regex.filtered = new RegExp(wo.filter_filteredRow); 554 | 555 | // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 556 | if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false').length !== c.$headers.length) { 557 | // build filter row 558 | ts.filter.buildRow(table, c, wo); 559 | } 560 | 561 | c.$table.bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(event, filter) { 562 | c.$table.find('.' + ts.css.filterRow).toggle( !(wo.filter_hideEmpty && $.isEmptyObject(c.cache)) ); // fixes #450 563 | if ( !/(search|filter)/.test(event.type) ) { 564 | event.stopPropagation(); 565 | ts.filter.buildDefault(table, true); 566 | } 567 | if (event.type === 'filterReset') { 568 | ts.filter.searching(table, []); 569 | } else if (event.type === 'filterEnd') { 570 | ts.filter.buildDefault(table, true); 571 | } else { 572 | // send false argument to force a new search; otherwise if the filter hasn't changed, it will return 573 | filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : ''; 574 | if (/(update|add)/.test(event.type) && event.type !== "updateComplete") { 575 | // force a new search since content has changed 576 | c.lastCombinedFilter = null; 577 | } 578 | // pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input 579 | // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]); 580 | ts.filter.searching(table, filter, true); 581 | } 582 | return false; 583 | }); 584 | 585 | // reset button/link 586 | if (wo.filter_reset) { 587 | $(document) 588 | .undelegate(wo.filter_reset, 'click.tsfilter') 589 | .delegate(wo.filter_reset, 'click.tsfilter', function() { 590 | // trigger a reset event, so other functions (filterFormatter) know when to reset 591 | c.$table.trigger('filterReset'); 592 | }); 593 | } 594 | if (wo.filter_functions) { 595 | // column = column # (string) 596 | for (column in wo.filter_functions) { 597 | if (wo.filter_functions.hasOwnProperty(column) && typeof column === 'string') { 598 | $header = c.$headers.filter('[data-column="' + column + '"]:last'); 599 | options = ''; 600 | if (wo.filter_functions[column] === true && !$header.hasClass('filter-false')) { 601 | ts.filter.buildSelect(table, column); 602 | } else if (typeof column === 'string' && !$header.hasClass('filter-false')) { 603 | // add custom drop down list 604 | for (string in wo.filter_functions[column]) { 605 | if (typeof string === 'string') { 606 | options += options === '' ? 607 | '' : ''; 608 | options += ''; 609 | } 610 | } 611 | c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').append(options); 612 | } 613 | } 614 | } 615 | } 616 | // not really updating, but if the column has both the "filter-select" class & filter_functions set to true, 617 | // it would append the same options twice. 618 | ts.filter.buildDefault(table, true); 619 | 620 | ts.filter.bindSearch( table, c.$table.find('.' + ts.css.filter), true ); 621 | if (wo.filter_external) { 622 | ts.filter.bindSearch( table, wo.filter_external ); 623 | } 624 | 625 | if (wo.filter_hideFilters) { 626 | ts.filter.hideFilters(table, c); 627 | } 628 | 629 | // show processing icon 630 | if (c.showProcessing) { 631 | c.$table.bind('filterStart.tsfilter filterEnd.tsfilter', function(event, columns) { 632 | // only add processing to certain columns to all columns 633 | $header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() { 634 | return columns[$(this).data('column')] !== ''; 635 | }) : ''; 636 | ts.isProcessing(table, event.type === 'filterStart', columns ? $header : ''); 637 | }); 638 | } 639 | 640 | if (c.debug) { 641 | ts.benchmark("Applying Filter widget", time); 642 | } 643 | // add default values 644 | c.$table.bind('tablesorter-initialized pagerInitialized', function() { 645 | filters = ts.filter.setDefaults(table, c, wo) || []; 646 | if (filters.length) { 647 | ts.setFilters(table, filters, true); 648 | } 649 | c.$table.trigger('filterFomatterUpdate'); 650 | ts.filter.checkFilters(table, filters); 651 | }); 652 | // filter widget initialized 653 | wo.filter_initialized = true; 654 | c.$table.trigger('filterInit'); 655 | }, 656 | setDefaults: function(table, c, wo) { 657 | var isArray, saved, 658 | // get current (default) filters 659 | filters = ts.getFilters(table); 660 | if (wo.filter_saveFilters && ts.storage) { 661 | saved = ts.storage( table, 'tablesorter-filters' ) || []; 662 | isArray = $.isArray(saved); 663 | // make sure we're not just getting an empty array 664 | if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; } 665 | } 666 | c.$table.data('lastSearch', filters); 667 | return filters; 668 | }, 669 | buildRow: function(table, c, wo) { 670 | var column, $header, buildSelect, disabled, name, 671 | // c.columns defined in computeThIndexes() 672 | columns = c.columns, 673 | buildFilter = ''; 674 | for (column = 0; column < columns; column++) { 675 | buildFilter += ''; 676 | } 677 | c.$filters = $(buildFilter += '').appendTo( c.$table.find('thead').eq(0) ).find('td'); 678 | // build each filter input 679 | for (column = 0; column < columns; column++) { 680 | disabled = false; 681 | // assuming last cell of a column is the main column 682 | $header = c.$headers.filter('[data-column="' + column + '"]:last'); 683 | buildSelect = (wo.filter_functions && wo.filter_functions[column] && typeof wo.filter_functions[column] !== 'function') || 684 | $header.hasClass('filter-select'); 685 | // get data from jQuery data, metadata, headers option or header class name 686 | if (ts.getData) { 687 | // get data from jQuery data, metadata, headers option or header class name 688 | disabled = ts.getData($header[0], c.headers[column], 'filter') === 'false'; 689 | } else { 690 | // only class names and header options - keep this for compatibility with tablesorter v2.0.5 691 | disabled = (c.headers[column] && c.headers[column].hasOwnProperty('filter') && c.headers[column].filter === false) || 692 | $header.hasClass('filter-false'); 693 | } 694 | if (buildSelect) { 695 | buildFilter = $('').appendTo( c.$filters.eq(column) ); 710 | } 711 | if (buildFilter) { 712 | buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || ''); 713 | } 714 | } 715 | if (buildFilter) { 716 | // add filter class name 717 | name = ( $.isArray(wo.filter_cssFilter) ? 718 | (typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '') : 719 | wo.filter_cssFilter ) || ''; 720 | buildFilter.addClass( ts.css.filter + ' ' + name ).attr('data-column', column); 721 | if (disabled) { 722 | buildFilter.addClass('disabled')[0].disabled = true; // disabled! 723 | } 724 | } 725 | } 726 | }, 727 | bindSearch: function(table, $el, internal) { 728 | table = $(table)[0]; 729 | $el = $($el); // allow passing a selector string 730 | if (!$el.length) { return; } 731 | var c = table.config, 732 | wo = c.widgetOptions, 733 | $ext = wo.filter_$externalFilters; 734 | if (internal !== true) { 735 | // save anyMatch element 736 | wo.filter_$anyMatch = $el.filter('[data-column="all"]'); 737 | if ($ext && $ext.length) { 738 | wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); 739 | } else { 740 | wo.filter_$externalFilters = $el; 741 | } 742 | // update values (external filters added after table initialization) 743 | ts.setFilters(table, c.$table.data('lastSearch') || [], internal === false); 744 | } 745 | $el 746 | // use data attribute instead of jQuery data since the head is cloned without including the data/binding 747 | .attr('data-lastSearchTime', new Date().getTime()) 748 | .unbind('keyup search change') 749 | // include change for select - fixes #473 750 | .bind('keyup search change', function(event, filters) { 751 | $(this).attr('data-lastSearchTime', new Date().getTime()); 752 | // emulate what webkit does.... escape clears the filter 753 | if (event.which === 27) { 754 | this.value = ''; 755 | // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace 756 | } else if ( (typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch && this.value !== '') || 757 | ( event.type === 'keyup' && ( (event.which < 32 && event.which !== 8 && wo.filter_liveSearch === true && event.which !== 13) || 758 | ( event.which >= 37 && event.which <= 40 ) || (event.which !== 13 && wo.filter_liveSearch === false) ) ) ) { 759 | return; 760 | } 761 | // true flag tells getFilters to skip newest timed input 762 | ts.filter.searching( table, '', true ); 763 | }); 764 | c.$table.bind('filterReset', function(){ 765 | $el.val(''); 766 | }); 767 | }, 768 | checkFilters: function(table, filter, skipFirst) { 769 | var c = table.config, 770 | wo = c.widgetOptions, 771 | filterArray = $.isArray(filter), 772 | filters = (filterArray) ? filter : ts.getFilters(table, true), 773 | combinedFilters = (filters || []).join(''); // combined filter values 774 | // add filter array back into inputs 775 | if (filterArray) { 776 | ts.setFilters( table, filters, false, skipFirst !== true ); 777 | } 778 | if (wo.filter_hideFilters) { 779 | // show/hide filter row as needed 780 | c.$table.find('.' + ts.css.filterRow).trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' ); 781 | } 782 | // return if the last search is the same; but filter === false when updating the search 783 | // see example-widget-filter.html filter toggle buttons 784 | if (c.lastCombinedFilter === combinedFilters && filter !== false) { 785 | return; 786 | } else if (filter === false) { 787 | // force filter refresh 788 | c.lastCombinedFilter = null; 789 | } 790 | c.$table.trigger('filterStart', [filters]); 791 | if (c.showProcessing) { 792 | // give it time for the processing icon to kick in 793 | setTimeout(function() { 794 | ts.filter.findRows(table, filters, combinedFilters); 795 | return false; 796 | }, 30); 797 | } else { 798 | ts.filter.findRows(table, filters, combinedFilters); 799 | return false; 800 | } 801 | }, 802 | hideFilters: function(table, c) { 803 | var $filterRow, $filterRow2, timer; 804 | c.$table 805 | .find('.' + ts.css.filterRow) 806 | .addClass('hideme') 807 | .bind('mouseenter mouseleave', function(e) { 808 | // save event object - http://bugs.jquery.com/ticket/12140 809 | var event = e; 810 | $filterRow = $(this); 811 | clearTimeout(timer); 812 | timer = setTimeout(function() { 813 | if ( /enter|over/.test(event.type) ) { 814 | $filterRow.removeClass('hideme'); 815 | } else { 816 | // don't hide if input has focus 817 | // $(':focus') needs jQuery 1.6+ 818 | if ( $(document.activeElement).closest('tr')[0] !== $filterRow[0] ) { 819 | // don't hide row if any filter has a value 820 | if (c.lastCombinedFilter === '') { 821 | $filterRow.addClass('hideme'); 822 | } 823 | } 824 | } 825 | }, 200); 826 | }) 827 | .find('input, select').bind('focus blur', function(e) { 828 | $filterRow2 = $(this).closest('tr'); 829 | clearTimeout(timer); 830 | var event = e; 831 | timer = setTimeout(function() { 832 | // don't hide row if any filter has a value 833 | if (ts.getFilters(table).join('') === '') { 834 | $filterRow2[ event.type === 'focus' ? 'removeClass' : 'addClass']('hideme'); 835 | } 836 | }, 200); 837 | }); 838 | }, 839 | findRows: function(table, filters, combinedFilters) { 840 | if (table.config.lastCombinedFilter === combinedFilters) { return; } 841 | var cached, len, $rows, cacheIndex, rowIndex, tbodyIndex, $tbody, $cells, columnIndex, 842 | childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result, 843 | searchFiltered, filterMatched, showRow, time, 844 | anyMatch, iAnyMatch, rowArray, rowText, iRowText, rowCache, 845 | c = table.config, 846 | wo = c.widgetOptions, 847 | columns = c.columns, 848 | $tbodies = c.$tbodies, 849 | // anyMatch really screws up with these types of filters 850 | anyMatchNotAllowedTypes = [ 'range', 'notMatch', 'operators' ], 851 | // parse columns after formatter, in case the class is added at that point 852 | parsed = c.$headers.map(function(columnIndex) { 853 | return c.parsers && c.parsers[columnIndex] && c.parsers[columnIndex].parsed || ( ts.getData ? 854 | ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), c.headers[columnIndex], 'filter') === 'parsed' : 855 | $(this).hasClass('filter-parsed') ); 856 | }).get(); 857 | if (c.debug) { time = new Date(); } 858 | for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 859 | if ($tbodies.eq(tbodyIndex).hasClass(ts.css.info)) { continue; } // ignore info blocks, issue #264 860 | $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); 861 | // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel! 862 | $rows = $tbody.children('tr').not(c.selectorRemove); 863 | len = $rows.length; 864 | if (combinedFilters === '' || wo.filter_serversideFiltering) { 865 | $tbody.children().not('.' + c.cssChildRow).show().removeClass(wo.filter_filteredRow); 866 | } else { 867 | // optimize searching only through already filtered rows - see #313 868 | searchFiltered = true; 869 | lastSearch = c.lastSearch || c.$table.data('lastSearch') || []; 870 | $.each(filters, function(indx, val) { 871 | // check for changes from beginning of filter; but ignore if there is a logical "or" in the string 872 | searchFiltered = (val || '').indexOf(lastSearch[indx] || '') === 0 && searchFiltered && !/(\s+or\s+|\|)/g.test(val || ''); 873 | }); 874 | // can't search when all rows are hidden - this happens when looking for exact matches 875 | if (searchFiltered && $rows.filter(':visible').length === 0) { searchFiltered = false; } 876 | if ((wo.filter_$anyMatch && wo.filter_$anyMatch.length) || filters[c.columns]) { 877 | anyMatch = wo.filter_$anyMatch && wo.filter_$anyMatch.val() || filters[c.columns] || ''; 878 | if (c.sortLocaleCompare) { 879 | // replace accents 880 | anyMatch = ts.replaceAccents(anyMatch); 881 | } 882 | iAnyMatch = anyMatch.toLowerCase(); 883 | } 884 | 885 | // loop through the rows 886 | cacheIndex = 0; 887 | for (rowIndex = 0; rowIndex < len; rowIndex++) { 888 | childRow = $rows[rowIndex].className; 889 | // skip child rows & already filtered rows 890 | if ( ts.filter.regex.child.test(childRow) || (searchFiltered && ts.filter.regex.filtered.test(childRow)) ) { continue; } 891 | showRow = true; 892 | // *** nextAll/nextUntil not supported by Zepto! *** 893 | childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')'); 894 | // so, if "table.config.widgetOptions.filter_childRows" is true and there is 895 | // a match anywhere in the child row, then it will make the row visible 896 | // checked here so the option can be changed dynamically 897 | childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : ''; 898 | childRowText = wo.filter_ignoreCase ? childRowText.toLocaleLowerCase() : childRowText; 899 | $cells = $rows.eq(rowIndex).children('td'); 900 | 901 | if (anyMatch) { 902 | rowArray = $cells.map(function(i){ 903 | var txt; 904 | if (parsed[i]) { 905 | txt = c.cache[tbodyIndex].normalized[cacheIndex][i]; 906 | } else { 907 | txt = wo.filter_ignoreCase ? $(this).text().toLowerCase() : $(this).text(); 908 | if (c.sortLocaleCompare) { 909 | txt = ts.replaceAccents(txt); 910 | } 911 | } 912 | return txt; 913 | }).get(); 914 | rowText = rowArray.join(' '); 915 | iRowText = rowText.toLowerCase(); 916 | rowCache = c.cache[tbodyIndex].normalized[cacheIndex].join(' '); 917 | filterMatched = null; 918 | $.each(ts.filter.types, function(type, typeFunction) { 919 | if ($.inArray(type, anyMatchNotAllowedTypes) < 0) { 920 | matches = typeFunction( anyMatch, iAnyMatch, rowText, iRowText, rowCache, columns, table, wo, parsed, rowArray ); 921 | if (matches !== null) { 922 | filterMatched = matches; 923 | return false; 924 | } 925 | } 926 | }); 927 | if (filterMatched !== null) { 928 | showRow = filterMatched; 929 | } else { 930 | showRow = (iRowText + childRowText).indexOf(iAnyMatch) >= 0; 931 | } 932 | } 933 | 934 | for (columnIndex = 0; columnIndex < columns; columnIndex++) { 935 | // ignore if filter is empty or disabled 936 | if (filters[columnIndex]) { 937 | cached = c.cache[tbodyIndex].normalized[cacheIndex][columnIndex]; 938 | // check if column data should be from the cell or from parsed data 939 | if (wo.filter_useParsedData || parsed[columnIndex]) { 940 | exact = cached; 941 | } else { 942 | // using older or original tablesorter 943 | exact = $.trim($cells.eq(columnIndex).text()); 944 | exact = c.sortLocaleCompare ? ts.replaceAccents(exact) : exact; // issue #405 945 | } 946 | iExact = !ts.filter.regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact; 947 | result = showRow; // if showRow is true, show that row 948 | 949 | // replace accents - see #357 950 | filters[columnIndex] = c.sortLocaleCompare ? ts.replaceAccents(filters[columnIndex]) : filters[columnIndex]; 951 | // val = case insensitive, filters[columnIndex] = case sensitive 952 | iFilter = wo.filter_ignoreCase ? (filters[columnIndex] || '').toLocaleLowerCase() : filters[columnIndex]; 953 | if (wo.filter_functions && wo.filter_functions[columnIndex]) { 954 | if (wo.filter_functions[columnIndex] === true) { 955 | // default selector; no "filter-select" class 956 | result = (c.$headers.filter('[data-column="' + columnIndex + '"]:last').hasClass('filter-match')) ? 957 | iExact.search(iFilter) >= 0 : filters[columnIndex] === exact; 958 | } else if (typeof wo.filter_functions[columnIndex] === 'function') { 959 | // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object ) 960 | result = wo.filter_functions[columnIndex](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex)); 961 | } else if (typeof wo.filter_functions[columnIndex][filters[columnIndex]] === 'function') { 962 | // selector option function 963 | result = wo.filter_functions[columnIndex][filters[columnIndex]](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex)); 964 | } 965 | } else { 966 | filterMatched = null; 967 | // cycle through the different filters 968 | // filters return a boolean or null if nothing matches 969 | $.each(ts.filter.types, function(type, typeFunction) { 970 | matches = typeFunction( filters[columnIndex], iFilter, exact, iExact, cached, columnIndex, table, wo, parsed ); 971 | if (matches !== null) { 972 | filterMatched = matches; 973 | return false; 974 | } 975 | }); 976 | if (filterMatched !== null) { 977 | result = filterMatched; 978 | // Look for match, and add child row data for matching 979 | } else { 980 | exact = (iExact + childRowText).indexOf(iFilter); 981 | result = ( (!wo.filter_startsWith && exact >= 0) || (wo.filter_startsWith && exact === 0) ); 982 | } 983 | } 984 | showRow = (result) ? showRow : false; 985 | } 986 | } 987 | $rows[rowIndex].style.display = (showRow ? '' : 'none'); 988 | $rows.eq(rowIndex)[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow); 989 | if (childRow.length) { 990 | if (c.pager && c.pager.countChildRows || wo.pager_countChildRows || wo.filter_childRows) { 991 | childRow[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow); // see issue #396 992 | } 993 | childRow.toggle(showRow); 994 | } 995 | cacheIndex++; 996 | } 997 | } 998 | ts.processTbody(table, $tbody, false); 999 | } 1000 | c.lastCombinedFilter = combinedFilters; // save last search 1001 | c.lastSearch = filters; 1002 | c.$table.data('lastSearch', filters); 1003 | if (wo.filter_saveFilters && ts.storage) { 1004 | ts.storage( table, 'tablesorter-filters', filters ); 1005 | } 1006 | if (c.debug) { 1007 | ts.benchmark("Completed filter widget search", time); 1008 | } 1009 | c.$table.trigger('applyWidgets'); // make sure zebra widget is applied 1010 | c.$table.trigger('filterEnd'); 1011 | }, 1012 | buildSelect: function(table, column, updating, onlyavail) { 1013 | column = parseInt(column, 10); 1014 | var indx, rowIndex, tbodyIndex, len, currentValue, txt, $filters, 1015 | c = table.config, 1016 | wo = c.widgetOptions, 1017 | $tbodies = c.$tbodies, 1018 | arry = [], 1019 | node = c.$headers.filter('[data-column="' + column + '"]:last'), 1020 | // t.data('placeholder') won't work in jQuery older than 1.4.3 1021 | options = ''; 1022 | for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 1023 | len = c.cache[tbodyIndex].row.length; 1024 | // loop through the rows 1025 | for (rowIndex = 0; rowIndex < len; rowIndex++) { 1026 | // check if has class filtered 1027 | if (onlyavail && c.cache[tbodyIndex].row[rowIndex][0].className.match(wo.filter_filteredRow)) { continue; } 1028 | // get non-normalized cell content 1029 | if (wo.filter_useParsedData) { 1030 | arry.push( '' + c.cache[tbodyIndex].normalized[rowIndex][column] ); 1031 | } else { 1032 | node = c.cache[tbodyIndex].row[rowIndex][0].cells[column]; 1033 | if (node) { 1034 | arry.push( $.trim( node.textContent || node.innerText || $(node).text() ) ); 1035 | } 1036 | } 1037 | } 1038 | } 1039 | // get unique elements and sort the list 1040 | // if $.tablesorter.sortText exists (not in the original tablesorter), 1041 | // then natural sort the list otherwise use a basic sort 1042 | arry = $.grep(arry, function(value, indx) { 1043 | return $.inArray(value, arry) === indx; 1044 | }); 1045 | arry = (ts.sortNatural) ? arry.sort(function(a, b) { return ts.sortNatural(a, b); }) : arry.sort(true); 1046 | 1047 | // Get curent filter value 1048 | currentValue = c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').val(); 1049 | 1050 | // build option list 1051 | for (indx = 0; indx < arry.length; indx++) { 1052 | txt = arry[indx].replace(/\"/g, """); 1053 | // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346 1054 | options += arry[indx] !== '' ? '' : ''; 1056 | } 1057 | // update all selects in the same column (clone thead in sticky headers & any external selects) - fixes 473 1058 | $filters = ( c.$filters ? c.$filters : c.$table.children('thead') ).find('.' + ts.css.filter); 1059 | if (wo.filter_$externalFilters) { 1060 | $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters; 1061 | } 1062 | $filters.filter('select[data-column="' + column + '"]')[ updating ? 'html' : 'append' ](options); 1063 | }, 1064 | buildDefault: function(table, updating) { 1065 | var columnIndex, $header, 1066 | c = table.config, 1067 | wo = c.widgetOptions, 1068 | columns = c.columns; 1069 | // build default select dropdown 1070 | for (columnIndex = 0; columnIndex < columns; columnIndex++) { 1071 | $header = c.$headers.filter('[data-column="' + columnIndex + '"]:last'); 1072 | // look for the filter-select class; build/update it if found 1073 | if (($header.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[columnIndex] === true) && 1074 | !$header.hasClass('filter-false')) { 1075 | if (!wo.filter_functions) { wo.filter_functions = {}; } 1076 | wo.filter_functions[columnIndex] = true; // make sure this select gets processed by filter_functions 1077 | ts.filter.buildSelect(table, columnIndex, updating, $header.hasClass(wo.filter_onlyAvail)); 1078 | } 1079 | } 1080 | }, 1081 | searching: function(table, filter, skipFirst) { 1082 | if (typeof filter === 'undefined' || filter === true) { 1083 | var wo = table.config.widgetOptions; 1084 | // delay filtering 1085 | clearTimeout(wo.searchTimer); 1086 | wo.searchTimer = setTimeout(function() { 1087 | ts.filter.checkFilters(table, filter, skipFirst ); 1088 | }, wo.filter_liveSearch ? wo.filter_searchDelay : 10); 1089 | } else { 1090 | // skip delay 1091 | ts.filter.checkFilters(table, filter, skipFirst); 1092 | } 1093 | } 1094 | }; 1095 | 1096 | ts.getFilters = function(table, getRaw, setFilters, skipFirst) { 1097 | var i, $filters, $column, 1098 | filters = false, 1099 | c = table ? $(table)[0].config : '', 1100 | wo = table ? c.widgetOptions : ''; 1101 | if (getRaw !== true && wo && !wo.filter_columnFilters) { 1102 | return $(table).data('lastSearch'); 1103 | } 1104 | if (c) { 1105 | if (c.$filters) { 1106 | $filters = c.$filters.find('.' + ts.css.filter); 1107 | } 1108 | if (wo.filter_$externalFilters) { 1109 | $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters; 1110 | } 1111 | if ($filters && $filters.length) { 1112 | filters = setFilters || []; 1113 | for (i = 0; i < c.columns + 1; i++) { 1114 | $column = $filters.filter('[data-column="' + (i === c.columns ? 'all' : i) + '"]'); 1115 | if ($column.length) { 1116 | // move the latest search to the first slot in the array 1117 | $column = $column.sort(function(a, b){ 1118 | return $(b).attr('data-lastSearchTime') - $(a).attr('data-lastSearchTime'); 1119 | }); 1120 | if ($.isArray(setFilters)) { 1121 | // skip first (latest input) to maintain cursor position while typing 1122 | (skipFirst ? $column.slice(1) : $column).val( setFilters[i] ).trigger('change.tsfilter'); 1123 | } else { 1124 | filters[i] = $column.val() || ''; 1125 | // don't change the first... it will move the cursor 1126 | $column.slice(1).val( filters[i] ); 1127 | } 1128 | // save any match input dynamically 1129 | if (i === c.columns && $column.length) { 1130 | wo.filter_$anyMatch = $column; 1131 | } 1132 | } 1133 | } 1134 | } 1135 | } 1136 | if (filters.length === 0) { 1137 | filters = false; 1138 | } 1139 | return filters; 1140 | }; 1141 | 1142 | ts.setFilters = function(table, filter, apply, skipFirst) { 1143 | var c = table ? $(table)[0].config : '', 1144 | valid = ts.getFilters(table, true, filter, skipFirst); 1145 | if (c && apply) { 1146 | // ensure new set filters are applied, even if the search is the same 1147 | c.lastCombinedFilter = null; 1148 | c.$table.trigger('search', [filter, false]).trigger('filterFomatterUpdate'); 1149 | } 1150 | return !!valid; 1151 | }; 1152 | 1153 | // Widget: Sticky headers 1154 | // based on this awesome article: 1155 | // http://css-tricks.com/13465-persistent-headers/ 1156 | // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech 1157 | // ************************** 1158 | ts.addWidget({ 1159 | id: "stickyHeaders", 1160 | priority: 60, // sticky widget must be initialized after the filter widget! 1161 | options: { 1162 | stickyHeaders : '', // extra class name added to the sticky header row 1163 | stickyHeaders_attachTo : null, // jQuery selector or object to attach sticky header to 1164 | stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element 1165 | stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists 1166 | stickyHeaders_addResizeEvent : true, // trigger "resize" event on headers 1167 | stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header 1168 | stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs 1169 | }, 1170 | format: function(table, c, wo) { 1171 | // filter widget doesn't initialize on an empty table. Fixes #449 1172 | if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { 1173 | return; 1174 | } 1175 | var $cell, 1176 | $table = c.$table, 1177 | $attach = $(wo.stickyHeaders_attachTo), 1178 | $thead = $table.children('thead:first'), 1179 | $win = $attach.length ? $attach : $(window), 1180 | $header = $thead.children('tr').not('.sticky-false').children(), 1181 | innerHeader = '.' + ts.css.headerIn, 1182 | $tfoot = $table.find('tfoot'), 1183 | $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '', 1184 | stickyOffset = $attach.length ? 0 : $stickyOffset.length ? 1185 | $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0, 1186 | $stickyTable = wo.$sticky = $table.clone() 1187 | .addClass('containsStickyHeaders') 1188 | .css({ 1189 | position : $attach.length ? 'absolute' : 'fixed', 1190 | margin : 0, 1191 | top : stickyOffset, 1192 | left : 0, 1193 | visibility : 'hidden', 1194 | zIndex : wo.stickyHeaders_zIndex ? wo.stickyHeaders_zIndex : 2 1195 | }), 1196 | $stickyThead = $stickyTable.children('thead:first').addClass(ts.css.sticky + ' ' + wo.stickyHeaders), 1197 | $stickyCells, 1198 | laststate = '', 1199 | spacing = 0, 1200 | nonwkie = $table.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(navigator.userAgent), 1201 | resizeHeader = function() { 1202 | stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0; 1203 | spacing = 0; 1204 | // yes, I dislike browser sniffing, but it really is needed here :( 1205 | // webkit automatically compensates for border spacing 1206 | if (nonwkie) { 1207 | // Firefox & Opera use the border-spacing 1208 | // update border-spacing here because of demos that switch themes 1209 | spacing = parseInt($header.eq(0).css('border-left-width'), 10) * 2; 1210 | } 1211 | $stickyTable.css({ 1212 | left : $attach.length ? (parseInt($attach.css('padding-left'), 10) || 0) + parseInt(c.$table.css('padding-left'), 10) + 1213 | parseInt(c.$table.css('margin-left'), 10) + parseInt($table.css('border-left-width'), 10) : 1214 | $thead.offset().left - $win.scrollLeft() - spacing, 1215 | width: $table.width() 1216 | }); 1217 | $stickyCells.filter(':visible').each(function(i) { 1218 | var $cell = $header.filter(':visible').eq(i), 1219 | // some wibbly-wobbly... timey-wimey... stuff, to make columns line up in Firefox 1220 | offset = nonwkie && $(this).attr('data-column') === ( '' + parseInt(c.columns/2, 10) ) ? 1 : 0; 1221 | $(this) 1222 | .css({ 1223 | width: $cell.width() - spacing, 1224 | height: $cell.height() 1225 | }) 1226 | .find(innerHeader).width( $cell.find(innerHeader).width() - offset ); 1227 | }); 1228 | }; 1229 | // fix clone ID, if it exists - fixes #271 1230 | if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } 1231 | // clear out cloned table, except for sticky header 1232 | // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing 1233 | $stickyTable.find('thead:gt(0), tr.sticky-false, tbody, tfoot').hide(); 1234 | if (!wo.stickyHeaders_includeCaption) { 1235 | $stickyTable.find('caption').remove(); 1236 | } else { 1237 | $stickyTable.find('caption').css( 'margin-left', '-1px' ); 1238 | } 1239 | // issue #172 - find td/th in sticky header 1240 | $stickyCells = $stickyThead.children().children(); 1241 | $stickyTable.css({ height:0, width:0, padding:0, margin:0, border:0 }); 1242 | // remove resizable block 1243 | $stickyCells.find('.' + ts.css.resizer).remove(); 1244 | // update sticky header class names to match real header after sorting 1245 | $table 1246 | .addClass('hasStickyHeaders') 1247 | .bind('sortEnd.tsSticky', function() { 1248 | $header.filter(':visible').each(function(indx) { 1249 | $cell = $stickyCells.filter(':visible').eq(indx) 1250 | .attr('class', $(this).attr('class')) 1251 | // remove processing icon 1252 | .removeClass(ts.css.processing + ' ' + c.cssProcessing); 1253 | if (c.cssIcon) { 1254 | $cell 1255 | .find('.' + ts.css.icon) 1256 | .attr('class', $(this).find('.' + ts.css.icon).attr('class')); 1257 | } 1258 | }); 1259 | }) 1260 | .bind('pagerComplete.tsSticky', function() { 1261 | resizeHeader(); 1262 | }); 1263 | 1264 | ts.bindEvents(table, $stickyThead.children().children('.tablesorter-header')); 1265 | 1266 | // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. 1267 | $table.after( $stickyTable ); 1268 | // make it sticky! 1269 | $win.bind('scroll.tsSticky resize.tsSticky', function(event) { 1270 | if (!$table.is(':visible')) { return; } // fixes #278 1271 | var prefix = 'tablesorter-sticky-', 1272 | offset = $table.offset(), 1273 | captionHeight = (wo.stickyHeaders_includeCaption ? 0 : $table.find('caption').outerHeight(true)), 1274 | scrollTop = ($attach.length ? $attach.offset().top : $win.scrollTop()) + stickyOffset - captionHeight, 1275 | tableHeight = $table.height() - ($stickyTable.height() + ($tfoot.height() || 0)), 1276 | isVisible = (scrollTop > offset.top) && (scrollTop < offset.top + tableHeight) ? 'visible' : 'hidden', 1277 | cssSettings = { visibility : isVisible }; 1278 | if ($attach.length) { 1279 | cssSettings.top = $attach.scrollTop(); 1280 | } else { 1281 | // adjust when scrolling horizontally - fixes issue #143 1282 | cssSettings.left = $thead.offset().left - $win.scrollLeft() - spacing; 1283 | } 1284 | $stickyTable 1285 | .removeClass(prefix + 'visible ' + prefix + 'hidden') 1286 | .addClass(prefix + isVisible) 1287 | .css(cssSettings); 1288 | if (isVisible !== laststate || event.type === 'resize') { 1289 | // make sure the column widths match 1290 | resizeHeader(); 1291 | laststate = isVisible; 1292 | } 1293 | }); 1294 | if (wo.stickyHeaders_addResizeEvent) { 1295 | ts.addHeaderResizeEvent(table); 1296 | } 1297 | 1298 | // look for filter widget 1299 | if ($table.hasClass('hasFilters')) { 1300 | // scroll table into view after filtering, if sticky header is active - #482 1301 | $table.bind('filterEnd', function() { 1302 | // $(':focus') needs jQuery 1.6+ 1303 | var $td = $(document.activeElement).closest('td'), 1304 | column = $td.parent().children().index($td); 1305 | // only scroll if sticky header is active 1306 | if ($stickyTable.hasClass(ts.css.stickyVis)) { 1307 | // scroll to original table (not sticky clone) 1308 | window.scrollTo(0, $table.position().top); 1309 | // give same input/select focus 1310 | if (column >= 0) { 1311 | c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); 1312 | } 1313 | } 1314 | }); 1315 | ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); 1316 | } 1317 | 1318 | $table.trigger('stickyHeadersInit'); 1319 | 1320 | }, 1321 | remove: function(table, c, wo) { 1322 | c.$table 1323 | .removeClass('hasStickyHeaders') 1324 | .unbind('sortEnd.tsSticky pagerComplete.tsSticky') 1325 | .find('.' + ts.css.sticky).remove(); 1326 | if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table 1327 | // don't unbind if any table on the page still has stickyheaders applied 1328 | if (!$('.hasStickyHeaders').length) { 1329 | $(window).unbind('scroll.tsSticky resize.tsSticky'); 1330 | } 1331 | ts.addHeaderResizeEvent(table, false); 1332 | } 1333 | }); 1334 | 1335 | // Add Column resizing widget 1336 | // this widget saves the column widths if 1337 | // $.tablesorter.storage function is included 1338 | // ************************** 1339 | ts.addWidget({ 1340 | id: "resizable", 1341 | priority: 40, 1342 | options: { 1343 | resizable : true, 1344 | resizable_addLastColumn : false 1345 | }, 1346 | format: function(table, c, wo) { 1347 | if (c.$table.hasClass('hasResizable')) { return; } 1348 | c.$table.addClass('hasResizable'); 1349 | var $rows, $columns, $column, column, 1350 | storedSizes = {}, 1351 | $table = c.$table, 1352 | mouseXPosition = 0, 1353 | $target = null, 1354 | $next = null, 1355 | fullWidth = Math.abs($table.parent().width() - $table.width()) < 20, 1356 | stopResize = function() { 1357 | if (ts.storage && $target && $next) { 1358 | storedSizes = {}; 1359 | storedSizes[$target.index()] = $target.width(); 1360 | storedSizes[$next.index()] = $next.width(); 1361 | $target.width( storedSizes[$target.index()] ); 1362 | $next.width( storedSizes[$next.index()] ); 1363 | if (wo.resizable !== false) { 1364 | ts.storage(table, 'tablesorter-resizable', storedSizes); 1365 | } 1366 | } 1367 | mouseXPosition = 0; 1368 | $target = $next = null; 1369 | $(window).trigger('resize'); // will update stickyHeaders, just in case 1370 | }; 1371 | storedSizes = (ts.storage && wo.resizable !== false) ? ts.storage(table, 'tablesorter-resizable') : {}; 1372 | // process only if table ID or url match 1373 | if (storedSizes) { 1374 | for (column in storedSizes) { 1375 | if (!isNaN(column) && column < c.$headers.length) { 1376 | c.$headers.eq(column).width(storedSizes[column]); // set saved resizable widths 1377 | } 1378 | } 1379 | } 1380 | $rows = $table.children('thead:first').children('tr'); 1381 | // add resizable-false class name to headers (across rows as needed) 1382 | $rows.children().each(function() { 1383 | var canResize, 1384 | $column = $(this); 1385 | column = $column.attr('data-column'); 1386 | canResize = ts.getData( $column, c.headers[column], 'resizable') === "false"; 1387 | $rows.children().filter('[data-column="' + column + '"]')[canResize ? 'addClass' : 'removeClass']('resizable-false'); 1388 | }); 1389 | // add wrapper inside each cell to allow for positioning of the resizable target block 1390 | $rows.each(function() { 1391 | $column = $(this).children().not('.resizable-false'); 1392 | if (!$(this).find('.' + ts.css.wrapper).length) { 1393 | // Firefox needs this inner div to position the resizer correctly 1394 | $column.wrapInner('
'); 1395 | } 1396 | // don't include the last column of the row 1397 | if (!wo.resizable_addLastColumn) { $column = $column.slice(0,-1); } 1398 | $columns = $columns ? $columns.add($column) : $column; 1399 | }); 1400 | $columns 1401 | .each(function() { 1402 | var $column = $(this), 1403 | padding = parseInt($column.css('padding-right'), 10) + 10; // 10 is 1/2 of the 20px wide resizer grip 1404 | $column 1405 | .find('.' + ts.css.wrapper) 1406 | .append('
'); 1408 | }) 1409 | .bind('mousemove.tsresize', function(event) { 1410 | // ignore mousemove if no mousedown 1411 | if (mouseXPosition === 0 || !$target) { return; } 1412 | // resize columns 1413 | var leftEdge = event.pageX - mouseXPosition, 1414 | targetWidth = $target.width(); 1415 | $target.width( targetWidth + leftEdge ); 1416 | if ($target.width() !== targetWidth && fullWidth) { 1417 | $next.width( $next.width() - leftEdge ); 1418 | } 1419 | mouseXPosition = event.pageX; 1420 | }) 1421 | .bind('mouseup.tsresize', function() { 1422 | stopResize(); 1423 | }) 1424 | .find('.' + ts.css.resizer + ',.' + ts.css.grip) 1425 | .bind('mousedown', function(event) { 1426 | // save header cell and mouse position; closest() not supported by jQuery v1.2.6 1427 | $target = $(event.target).closest('th'); 1428 | var $header = c.$headers.filter('[data-column="' + $target.attr('data-column') + '"]'); 1429 | if ($header.length > 1) { $target = $target.add($header); } 1430 | // if table is not as wide as it's parent, then resize the table 1431 | $next = event.shiftKey ? $target.parent().find('th').not('.resizable-false').filter(':last') : $target.nextAll(':not(.resizable-false)').eq(0); 1432 | mouseXPosition = event.pageX; 1433 | }); 1434 | $table.find('thead:first') 1435 | .bind('mouseup.tsresize mouseleave.tsresize', function() { 1436 | stopResize(); 1437 | }) 1438 | // right click to reset columns to default widths 1439 | .bind('contextmenu.tsresize', function() { 1440 | ts.resizableReset(table); 1441 | // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset 1442 | var allowClick = $.isEmptyObject ? $.isEmptyObject(storedSizes) : true; 1443 | storedSizes = {}; 1444 | return allowClick; 1445 | }); 1446 | }, 1447 | remove: function(table, c) { 1448 | c.$table 1449 | .removeClass('hasResizable') 1450 | .children('thead') 1451 | .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize') 1452 | .children('tr').children() 1453 | .unbind('mousemove.tsresize mouseup.tsresize') 1454 | // don't remove "tablesorter-wrapper" as uitheme uses it too 1455 | .find('.' + ts.css.resizer + ',.' + ts.css.grip).remove(); 1456 | ts.resizableReset(table); 1457 | } 1458 | }); 1459 | ts.resizableReset = function(table) { 1460 | $(table).each(function(){ 1461 | this.config.$headers.not('.resizable-false').css('width',''); 1462 | if (ts.storage) { ts.storage(this, 'tablesorter-resizable', {}); } 1463 | }); 1464 | }; 1465 | 1466 | // Save table sort widget 1467 | // this widget saves the last sort only if the 1468 | // saveSort widget option is true AND the 1469 | // $.tablesorter.storage function is included 1470 | // ************************** 1471 | ts.addWidget({ 1472 | id: 'saveSort', 1473 | priority: 20, 1474 | options: { 1475 | saveSort : true 1476 | }, 1477 | init: function(table, thisWidget, c, wo) { 1478 | // run widget format before all other widgets are applied to the table 1479 | thisWidget.format(table, c, wo, true); 1480 | }, 1481 | format: function(table, c, wo, init) { 1482 | var stored, time, 1483 | $table = c.$table, 1484 | saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true 1485 | sortList = { "sortList" : c.sortList }; 1486 | if (c.debug) { 1487 | time = new Date(); 1488 | } 1489 | if ($table.hasClass('hasSaveSort')) { 1490 | if (saveSort && table.hasInitialized && ts.storage) { 1491 | ts.storage( table, 'tablesorter-savesort', sortList ); 1492 | if (c.debug) { 1493 | ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time); 1494 | } 1495 | } 1496 | } else { 1497 | // set table sort on initial run of the widget 1498 | $table.addClass('hasSaveSort'); 1499 | sortList = ''; 1500 | // get data 1501 | if (ts.storage) { 1502 | stored = ts.storage( table, 'tablesorter-savesort' ); 1503 | sortList = (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : ''; 1504 | if (c.debug) { 1505 | ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time); 1506 | } 1507 | $table.bind('saveSortReset', function(event) { 1508 | event.stopPropagation(); 1509 | ts.storage( table, 'tablesorter-savesort', '' ); 1510 | }); 1511 | } 1512 | // init is true when widget init is run, this will run this widget before all other widgets have initialized 1513 | // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. 1514 | if (init && sortList && sortList.length > 0) { 1515 | c.sortList = sortList; 1516 | } else if (table.hasInitialized && sortList && sortList.length > 0) { 1517 | // update sort change 1518 | $table.trigger('sorton', [sortList]); 1519 | } 1520 | } 1521 | }, 1522 | remove: function(table) { 1523 | // clear storage 1524 | if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } 1525 | } 1526 | }); 1527 | 1528 | })(jQuery); 1529 | -------------------------------------------------------------------------------- /src/main/resources/static/js/simpleblog.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | $('.tablesorter-bootstrap').tablesorter({ 3 | theme : 'bootstrap', 4 | headerTemplate: '{content} {icon}', 5 | widgets : ['zebra','columns', 'uitheme'] 6 | }); 7 | }); --------------------------------------------------------------------------------