aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alex Bitney2016-02-07 00:00:41 +0200
committerGravatar Alex Bitney2016-02-07 00:00:41 +0200
commitc2d316449f85bd2b74ae9ffaa3d08b7a5ee282cf (patch)
treece808c6d65e4a61913497f5d397473f75fadc701
parent1fffebc18bbdbe87b456e2b3bd66e9ce5a6afcb8 (diff)
added tags when posting new message
added templates engine (rythm engine) and moved something to it. WARNING: textext plugin does not work when minimized, and also I fixed bug in it.
-rw-r--r--build.gradle64
-rw-r--r--src/main/java/com/juick/JuickApplication.java56
-rw-r--r--src/main/java/com/juick/http/www/Home.java13
-rw-r--r--src/main/java/com/juick/http/www/Main.java4
-rw-r--r--src/main/java/com/juick/http/www/NewMessage.java33
-rw-r--r--src/main/java/com/juick/http/www/PageTemplates.java80
-rw-r--r--src/main/java/com/juick/http/www/User.java73
-rw-r--r--src/main/java/com/juick/http/www/Utils.java1
-rw-r--r--src/main/resources/templates/parts/page_header.html33
-rw-r--r--src/main/resources/templates/parts/page_navigation.html40
-rw-r--r--src/main/resources/templates/parts/post_form.html64
-rw-r--r--src/main/resources/templates/parts/test.html3
-rw-r--r--src/main/webapp/WEB-INF/web.xml12
-rw-r--r--src/main/webapp/scripts.js15
-rw-r--r--src/main/webapp/style.css2
-rw-r--r--src/main/webapp/test.json19
-rw-r--r--src/main/webapp/test.testhtml45
-rw-r--r--src/main/webapp/textext/textext.core.css29
-rw-r--r--src/main/webapp/textext/textext.core.js1617
-rw-r--r--src/main/webapp/textext/textext.plugin.ajax.js354
-rw-r--r--src/main/webapp/textext/textext.plugin.arrow.css13
-rw-r--r--src/main/webapp/textext/textext.plugin.arrow.js106
-rw-r--r--src/main/webapp/textext/textext.plugin.autocomplete.css35
-rw-r--r--src/main/webapp/textext/textext.plugin.autocomplete.js1110
-rw-r--r--src/main/webapp/textext/textext.plugin.filter.js242
-rw-r--r--src/main/webapp/textext/textext.plugin.focus.css12
-rw-r--r--src/main/webapp/textext/textext.plugin.focus.js174
-rw-r--r--src/main/webapp/textext/textext.plugin.prompt.css16
-rw-r--r--src/main/webapp/textext/textext.plugin.prompt.js309
-rw-r--r--src/main/webapp/textext/textext.plugin.suggestions.js175
-rw-r--r--src/main/webapp/textext/textext.plugin.tags.css49
-rw-r--r--src/main/webapp/textext/textext.plugin.tags.js692
32 files changed, 5360 insertions, 130 deletions
diff --git a/build.gradle b/build.gradle
index 7a795da3..10dcdd82 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,10 +1,3 @@
-subprojects {
- apply plugin: 'java'
- repositories {
- mavenCentral()
- }
-}
-
buildscript {
repositories {
mavenCentral()
@@ -18,12 +11,34 @@ buildscript {
}
}
+plugins {
+ id "com.eriwen.gradle.js" version "1.12.1"
+}
+
+subprojects {
+ apply plugin: 'java'
+ repositories {
+ mavenCentral()
+ }
+}
+
apply plugin: 'war'
apply plugin: 'css'
apply plugin: 'com.bmuschko.tomcat'
apply plugin: 'com.eriwen.gradle.js'
+apply plugin: 'com.eriwen.gradle.css'
apply plugin: 'com.github.tkruse.groovysh'
+////ext.environment = hasProperty('env') ? env : 'dev'
+//environments {
+// dev {
+//
+// }
+// prod {
+//
+// }
+//}
+
repositories {
mavenCentral()
}
@@ -47,7 +62,7 @@ project(':deps:com.juick.xmpp') {
project(':deps:com.juick.json') {
dependencies {
- compile core
+ compile core
}
}
@@ -63,6 +78,7 @@ dependencies {
compile 'org.apache.httpcomponents:httpclient:4.5.1'
compile 'org.json:json:20151123'
compile "org.springframework:spring-jdbc:4.2.4.RELEASE"
+ compile 'org.rythmengine:rythm-engine:1.0.1'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
runtime 'mysql:mysql-connector-java:5.1.37'
def tomcatVersion = '7.0.+'
@@ -72,16 +88,22 @@ dependencies {
}
javascript.source {
- dev {
+ juick {
js {
srcDir "src/main/webapp"
include "*.js"
}
}
+ textext {
+ js {
+ srcDir "src/main/webapp/textext"
+ include "*.js"
+ }
+ }
}
combineJs {
- source = javascript.source.dev.js.files
+ source = javascript.source.juick.js.files + javascript.source.textext.js.files
dest = file("${buildDir}/scripts.all.js")
}
@@ -89,19 +111,28 @@ minifyJs {
source = combineJs
dest = file("${buildDir}/scripts.min.js")
sourceMap = file("${buildDir}/scripts.sourcemap.json")
+ println("webAppDirName=${webAppDir} - ${webAppDirName}")
+
}
+
css.source {
- dev {
+ juick {
css {
srcDir "src/main/webapp"
include "*.css"
}
}
+ textext {
+ css {
+ srcDir "src/main/webapp/textext"
+ include "*.css"
+ }
+ }
}
combineCss {
- source = css.source.dev.css.files
+ source = css.source.juick.css.files + css.source.textext.css.files
dest = "${buildDir}/style.all.css"
}
@@ -110,6 +141,15 @@ minifyCss {
dest = "${buildDir}/style.min.css"
}
+war {
+ it.dependsOn minifyCss
+ it.dependsOn minifyJs
+ from "${buildDir}/scripts.min.js"
+ from "${buildDir}/style.min.css"
+ from "${buildDir}/scripts.all.js"
+ from "${buildDir}/style.all.css"
+}
+
assemble.dependsOn 'minifyCss'
assemble.dependsOn 'minifyJs'
diff --git a/src/main/java/com/juick/JuickApplication.java b/src/main/java/com/juick/JuickApplication.java
index ce242222..e546f5a8 100644
--- a/src/main/java/com/juick/JuickApplication.java
+++ b/src/main/java/com/juick/JuickApplication.java
@@ -4,6 +4,7 @@ import com.juick.xmpp.JID;
import com.juick.xmpp.Stream;
import com.juick.xmpp.StreamComponent;
import com.juick.xmpp.s2s.S2SComponent;
+import org.rythmengine.Rythm;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@@ -12,9 +13,7 @@ import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.Channels;
import java.nio.channels.CompletionHandler;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
+import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
@@ -32,17 +31,34 @@ public class JuickApplication {
private JdbcTemplate sql;
private JdbcTemplate sqlSearch;
private List<JuickComponent> components = new ArrayList<>();
+ private String applicationPath;
- public JuickApplication(Properties conf) throws IOException {
+ public JuickApplication(Properties conf, String applicationPath) throws IOException {
+ this.applicationPath = applicationPath;
executorService = Executors.newWorkStealingPool();
- DriverManagerDataSource dataSource = new DriverManagerDataSource();
- dataSource.setDriverClassName(conf.getProperty("datasource_driver", "com.mysql.jdbc.Driver"));
- dataSource.setUrl(conf.getProperty("datasource_url"));
- sql = new JdbcTemplate(dataSource);
- DriverManagerDataSource searchDatasource = new DriverManagerDataSource();
- searchDatasource.setDriverClassName("com.mysql.jdbc.Driver");
- searchDatasource.setUrl("jdbc:mysql://127.0.0.1:9306?autoReconnect=true&useUnicode=yes&characterEncoding=utf8&maxAllowedPacket=512000");
- sqlSearch = new JdbcTemplate(searchDatasource);
+ initDataSources(conf);
+ initSockets(conf);
+ initComponents(conf);
+ initRythm(conf);
+ }
+
+ private void initRythm(Properties conf) {
+ String rythmMode = conf.getProperty("rythm_mode", "prod");
+ Map<String, Object> map = new HashMap<>();
+ map.put("home.template", applicationPath+"/WEB-INF/classes/templates");
+ map.put("rythm.engine.mode", rythmMode);
+ Rythm.init(map);
+ }
+
+ private void initComponents(Properties conf) {
+ if (!isHttpDevMode()) {
+ addComponent(new S2SComponent(this, conf));
+ addComponent(new CrosspostComponent(sql, conf));
+ addComponent(new PushComponent(sql, conf));
+ }
+ }
+
+ private void initSockets(final Properties conf) throws IOException {
AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
socket.connect(new InetSocketAddress("localhost", 5347), socket,
new CompletionHandler<Void, AsynchronousSocketChannel>() {
@@ -60,11 +76,17 @@ public class JuickApplication {
}
}
});
- if (!isHttpDevMode()) {
- addComponent(new S2SComponent(this, conf));
- addComponent(new CrosspostComponent(sql, conf));
- addComponent(new PushComponent(sql, conf));
- }
+ }
+
+ private void initDataSources(Properties conf) {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName(conf.getProperty("datasource_driver", "com.mysql.jdbc.Driver"));
+ dataSource.setUrl(conf.getProperty("datasource_url"));
+ sql = new JdbcTemplate(dataSource);
+ DriverManagerDataSource searchDatasource = new DriverManagerDataSource();
+ searchDatasource.setDriverClassName("com.mysql.jdbc.Driver");
+ searchDatasource.setUrl("jdbc:mysql://127.0.0.1:9306?autoReconnect=true&useUnicode=yes&characterEncoding=utf8&maxAllowedPacket=512000");
+ sqlSearch = new JdbcTemplate(searchDatasource);
}
public static boolean isHttpDevMode() {
diff --git a/src/main/java/com/juick/http/www/Home.java b/src/main/java/com/juick/http/www/Home.java
index 3688b58e..b8047190 100644
--- a/src/main/java/com/juick/http/www/Home.java
+++ b/src/main/java/com/juick/http/www/Home.java
@@ -19,6 +19,7 @@ package com.juick.http.www;
import com.juick.server.AdsQueries;
import com.juick.server.MessagesQueries;
+import org.rythmengine.Rythm;
import org.springframework.jdbc.core.JdbcTemplate;
import java.io.IOException;
@@ -119,17 +120,9 @@ public class Home {
out.println("<!--noindex-->");
}
+
if (visitor != null) {
- out.println("<form action=\"/post\" method=\"post\" enctype=\"multipart/form-data\" onsubmit=\"return onsubmitNewMessage()\">");
- out.println("<section id=\"newmessage\">");
- out.println(" <textarea name=\"body\" placeholder=\"Новое сообщение...\" onclick=\"$('#newmessage>div').css('display','block');$('#newmessage textarea').css('min-height','70px');\" onkeypress=\"postformListener(this.form,event)\"></textarea>");
- out.println(" <div>");
- out.println(" <input type=\"text\" class=\"img\" name=\"img\" placeholder=\"Ссылка на изображение (JPG/PNG, до 10Мб)\"/> или <a href=\"#\" onclick=\"return attachMessagePhoto(this)\">загрузить</a><br/>");
- out.println(" <input type=\"text\" class=\"tags\" name=\"tags\" placeholder=\"Теги (через пробел)\"/><br/>");
- out.println(" <input type=\"submit\" class=\"subm\" value=\"Отправить\"/>");
- out.println(" </div>");
- out.println("</section>");
- out.println("</form>");
+ out.println(Rythm.render("parts/post_form.html"));
}
if (mids.size() > 0) {
diff --git a/src/main/java/com/juick/http/www/Main.java b/src/main/java/com/juick/http/www/Main.java
index f37ae030..babeee1f 100644
--- a/src/main/java/com/juick/http/www/Main.java
+++ b/src/main/java/com/juick/http/www/Main.java
@@ -77,7 +77,7 @@ public class Main extends HttpServlet implements Stream.StreamListener {
conf.getProperty("twitter_consumer_secret"));
PageTemplates.sape = new Sape(conf.getProperty("sape_user"), "juick.com", 2000, 3600);
Endpoints.wwwJuickCom = conf.getProperty("www_juick_com", "juick.com");
- app = new JuickApplication(conf);
+ app = new JuickApplication(conf, getServletContext().getRealPath("/"));
sql = app.getSql();
sqlSearch = app.getSqlSearch();
pagesNewMessage = new NewMessage(app);
@@ -171,6 +171,8 @@ public class Main extends HttpServlet implements Stream.StreamListener {
help.doRedirectToHelpIndex(sql, request, response);
} else if (uri.startsWith("/help/")) {
help.doGetHelp(sql, request, response);
+ } else if (uri.startsWith("/my_tags.json")) {
+ pagesUser.doMyTagsJson(sql, request, response);
} else if (uri.startsWith("/tag/")) {
discover.doGet(sql, sqlSearch, request, response);
} else if (uri.matches("^/\\d+$")) {
diff --git a/src/main/java/com/juick/http/www/NewMessage.java b/src/main/java/com/juick/http/www/NewMessage.java
index d3178c59..c8eb6ede 100644
--- a/src/main/java/com/juick/http/www/NewMessage.java
+++ b/src/main/java/com/juick/http/www/NewMessage.java
@@ -17,13 +17,10 @@
*/
package com.juick.http.www;
+import com.alibaba.fastjson.JSONArray;
import com.juick.JuickApplication;
import com.juick.Tag;
-import com.juick.server.CrosspostQueries;
-import com.juick.server.MessagesQueries;
-import com.juick.server.SubscriptionsQueries;
-import com.juick.server.TagQueries;
-import com.juick.server.UserQueries;
+import com.juick.server.*;
import com.juick.xmpp.JID;
import com.juick.xmpp.Message;
import com.juick.xmpp.Stream;
@@ -161,13 +158,25 @@ public class NewMessage {
List<com.juick.Tag> tags = new ArrayList<>();
String tagsArr[] = new String[1];
if (tagsStr != null && !tagsStr.isEmpty()) {
- tagsArr = tagsStr.split("[ \\,]");
- for (int i = 0; i < tagsArr.length; i++) {
- if (tagsArr[i].startsWith("*")) {
- tagsArr[i] = tagsArr[i].substring(1);
+ if (tagsStr.startsWith("[")) {
+ // new json tags format with auto-completion
+ JSONArray parse = (JSONArray)JSONArray.parse(tagsStr);
+ ArrayList<String> tagsList = new ArrayList<>();
+ for(int i=0; i<parse.size(); i++) {
+ if (parse.get(i) instanceof String) {
+ tagsList.add(parse.getString(i));
+ }
}
- if (tagsArr[i].length() > 64) {
- tagsArr[i] = tagsArr[i].substring(0, 64);
+ tagsArr = tagsList.toArray(new String[tagsList.size()]);
+ } else {
+ tagsArr = tagsStr.split("[ \\,]");
+ for (int i = 0; i < tagsArr.length; i++) {
+ if (tagsArr[i].startsWith("*")) {
+ tagsArr[i] = tagsArr[i].substring(1);
+ }
+ if (tagsArr[i].length() > 64) {
+ tagsArr[i] = tagsArr[i].substring(0, 64);
+ }
}
}
tags = TagQueries.getTags(sql, tagsArr, true);
@@ -289,7 +298,7 @@ public class NewMessage {
out.println("<p><a href=\"https://www.facebook.com/sharer/sharer.php?u=" + url + "\" onclick=\"return openSocialWindow(this)\" class=\"ico32-fb sharenew\">Отправить в Facebook</a></p>");
}
out.println("<p><a href=\"https://plus.google.com/share?url=" + url + "\" onclick=\"return openSocialWindow(this)\" class=\"ico32-gp sharenew\">Отправить в Google+</a></p>");
- out.println("<p>Ссылка на сообщение: <a href=\"http://juick.com/" + mid + "\">http://juick.com/" + mid + "</a></p>");
+ out.println("<p>Ссылка на сообщение: <a href=\"http://"+ Endpoints.wwwJuickCom+"/" + mid + "\">http://"+Endpoints.wwwJuickCom+"/" + mid + "</a></p>");
out.println("</section>");
PageTemplates.pageFooter(request, out, visitor, false);
diff --git a/src/main/java/com/juick/http/www/PageTemplates.java b/src/main/java/com/juick/http/www/PageTemplates.java
index 3eb66877..2ae945a5 100644
--- a/src/main/java/com/juick/http/www/PageTemplates.java
+++ b/src/main/java/com/juick/http/www/PageTemplates.java
@@ -22,6 +22,7 @@ import com.juick.Tag;
import com.juick.server.Endpoints;
import com.juick.server.MessagesQueries;
import com.juick.server.UserQueries;
+import org.rythmengine.Rythm;
import org.springframework.jdbc.core.JdbcTemplate;
import ru.sape.Sape;
@@ -51,58 +52,43 @@ public class PageTemplates {
private static String tagsHTML = null;
public static void pageHead(PrintWriter out, String title, String headers) {
- out.println("<!DOCTYPE html>");
- out.print("<html>");
- out.print("<head>");
- out.println("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">");
- out.print("<link rel=\"stylesheet\" href=\"/style.css\"/>");
- out.print("<script type=\"text/javascript\" src=\"//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js\"></script>");
- out.print("<script type=\"text/javascript\" src=\"/scripts.js\"></script>");
- if (headers != null) {
- out.print(headers);
- }
- out.print("<title>" + title + "</title>");
- out.println("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,user-scalable=no\"/>");
- out.println("<link rel=\"icon\" href=\"//i.juick.com/favicon.png\"/>");
- out.println("<!--[if lt IE 9 & (!IEMobile 7)]>");
- out.println("<script src=\"//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js\"></script>");
- out.println("<![endif]-->");
- out.println("</head>");
+ out.print(Rythm.render("parts/page_header.html", headers, title));
out.flush();
out.println("<body>");
}
public static void pageNavigation(PrintWriter out, com.juick.User visitor, String search) {
- out.println("<header>");
- out.println(" <div id=\"logo\"><a href=\"/\">Juick</a></div>");
- out.print(" <nav id=\"global\"><ul>");
- out.print("<li><a href=\"/\">Популярные</a></li>");
- out.print("<li><a href=\"/?show=all\" rel=\"nofollow\">Все сообщения</a></li>");
- out.print("<li><a href=\"/?show=photos\" rel=\"nofollow\">Фотографии</a></li>");
- out.println("</ul></nav>");
- out.print(" <div id=\"search\"><form action=\"/\"><input type=\"text\" name=\"search\" class=\"text\" placeholder=\"Поиск\"");
- if (search != null) {
- out.print(" value=\"" + Utils.encodeHTML(search) + "\"");
- }
- out.println("/></form></div>");
- out.println(" <section id=\"headdiv\">");
- if (visitor != null) {
- out.print(" <nav id=\"user\"><ul>");
- out.print("<li><a href=\"/?show=my\">Моя лента</a></li>");
- out.print("<li><a href=\"/pm/inbox\">Приватные</a></li>");
- out.print("<li><a href=\"/?show=discuss\">Обсуждения</a></li>");
- out.print("<li><a href=\"/?show=recommended\">Рекомендации</a></li>");
- out.println("</ul></nav>");
- out.print(" <nav id=\"actions\"><ul>");
- out.print("<li><a href=\"/#post\">Написать</a></li>");
- out.print("<li><a href=\"/" + visitor.getUName() + "\">@" + visitor.getUName() + "</a></li>");
- out.print("<li><a href=\"/logout\">Выйти</a></li>");
- out.println("</ul></nav>");
- } else {
- out.println("<p>Чтобы добавлять сообщения и комментарии, <a href=\"#\" onclick=\"return openDialogLogin()\">представьтесь</a>.</p>");
- }
- out.println(" </section>");
- out.println("</header>");
+ out.print(Rythm.render("parts/page_navigation.html", search, visitor));
+// out.println("<header>");
+// out.println(" <div id=\"logo\"><a href=\"/\">Juick</a></div>");
+// out.print(" <nav id=\"global\"><ul>");
+// out.print("<li><a href=\"/\">Популярные</a></li>");
+// out.print("<li><a href=\"/?show=all\" rel=\"nofollow\">Все сообщения</a></li>");
+// out.print("<li><a href=\"/?show=photos\" rel=\"nofollow\">Фотографии</a></li>");
+// out.println("</ul></nav>");
+// out.print(" <div id=\"search\"><form action=\"/\"><input type=\"text\" name=\"search\" class=\"text\" placeholder=\"Поиск\"");
+// if (search != null) {
+// out.print(" value=\"" + Utils.encodeHTML(search) + "\"");
+// }
+// out.println("/></form></div>");
+// out.println(" <section id=\"headdiv\">");
+// if (visitor != null) {
+// out.print(" <nav id=\"user\"><ul>");
+// out.print("<li><a href=\"/?show=my\">Моя лента</a></li>");
+// out.print("<li><a href=\"/pm/inbox\">Приватные</a></li>");
+// out.print("<li><a href=\"/?show=discuss\">Обсуждения</a></li>");
+// out.print("<li><a href=\"/?show=recommended\">Рекомендации</a></li>");
+// out.println("</ul></nav>");
+// out.print(" <nav id=\"actions\"><ul>");
+// out.print("<li><a href=\"/#post\">Написать</a></li>");
+// out.print("<li><a href=\"/" + visitor.getUName() + "\">@" + visitor.getUName() + "</a></li>");
+// out.print("<li><a href=\"/logout\">Выйти</a></li>");
+// out.println("</ul></nav>");
+// } else {
+// out.println("<p>Чтобы добавлять сообщения и комментарии, <a href=\"#\" onclick=\"return openDialogLogin()\">представьтесь</a>.</p>");
+// }
+// out.println(" </section>");
+// out.println("</header>");
}
public static void pageYandexAd728(PrintWriter out, int YandexID) {
diff --git a/src/main/java/com/juick/http/www/User.java b/src/main/java/com/juick/http/www/User.java
index 1f2b7dc0..5d06045c 100644
--- a/src/main/java/com/juick/http/www/User.java
+++ b/src/main/java/com/juick/http/www/User.java
@@ -17,25 +17,21 @@
*/
package com.juick.http.www;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.JSONSerializer;
import com.juick.Tag;
import com.juick.server.Endpoints;
import com.juick.server.MessagesQueries;
import com.juick.server.TagQueries;
import com.juick.server.UserQueries;
-import com.juick.xmpp.Stream;
import org.apache.commons.lang3.tuple.Pair;
+import org.json.JSONArray;
+import org.json.JSONObject;
import org.springframework.jdbc.core.JdbcTemplate;
import java.io.IOException;
import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
@@ -183,7 +179,7 @@ public class User {
pageUserColumn(out, sql, user, visitor);
out.println("<section id=\"content\">");
- out.println("<p>" + pageUserTags(sql, user, visitor, 0) + "</p>");
+ out.println("<p>" + pageUserTags(sql, user, 0) + "</p>");
out.println("</section>");
PageTemplates.pageFooter(request, out, visitor, false);
@@ -306,12 +302,16 @@ public class User {
out.println(" <li><a href=\"./\">Блог</a></li>");
out.println(" <li><a href=\"./?show=recomm\" rel=\"nofollow\">Рекомендации</a></li>");
out.println(" <li><a href=\"./?show=photos\" rel=\"nofollow\">Фотографии</a></li>");
+ if (visitor != null && visitor.getUID() == user.getUID()) {
+ out.println(" <li><a href=\"/?show=mycomments\" rel=\"nofollow\">Мои комментарии</a></li>");
+ out.println(" <li><a href=\"/?show=unanswered\" rel=\"nofollow\">Неотвеченные</a></li>");
+ }
out.println(" </ul>");
out.println(" <hr/>");
out.println(" <form action=\"./\">");
out.println(" <p><input type=\"text\" name=\"search\" class=\"inp\" placeholder=\"Поиск\"/></p>");
out.println(" </form>");
- out.println(" <p class=\"tags\">" + pageUserTags(sql, user, visitor, 20) + "<a href=\"./tags\" rel=\"nofollow\">...</a></p>");
+ out.println(" <p class=\"tags\">" + pageUserTags(sql, user, 20) + "<a href=\"./tags\" rel=\"nofollow\">...</a></p>");
out.println(" <hr/>");
out.println(" <div id=\"ustats\"><ul>");
out.println(" <li><a href=\"./friends\">Я читаю: " + UserQueries.getStatsIRead(sql, user.getUID()) + "</a></li>");
@@ -337,7 +337,28 @@ public class User {
out.println("</aside>");
}
- public static String pageUserTags(JdbcTemplate sql, com.juick.User user, com.juick.User visitor, int cnt) {
+ public static String pageUserTags(JdbcTemplate sql, com.juick.User user, int cnt) {
+ List<Tag> tags = getUserTags(sql, user, cnt);
+
+ int maxUsageCnt = tags.size() > 0 ? tags.stream().mapToInt(tag -> tag.UsageCnt).max().getAsInt() : 1;
+
+ return tags.stream().map(tag -> "<a href=\"./?tag=" + URLEncoder.encode(tag.Name) +
+ "\" title=\"" + tag.UsageCnt + "\" rel=\"nofollow\">" + tag.Name + "</a>")
+ .collect(Collectors.joining(" "));
+
+ /*
+ todo:
+ if (tags[i].UsageCnt > maxUsageCnt / 3 * 2) {
+ ret += "<big>" + tag + "</big> ";
+ } else if (tags[i].UsageCnt > maxUsageCnt / 3) {
+ ret += "<small>" + tag + "</small> ";
+ } else {
+ ret += tag + " ";
+ }
+ }*/
+ }
+
+ public static List<Tag> getUserTags(JdbcTemplate sql, com.juick.User user, int cnt) {
List<Tag> tags;
if (cnt > 0) {
tags = sql.query("SELECT tags.name AS name,COUNT(DISTINCT messages_tags.message_id) AS cnt " +
@@ -361,22 +382,22 @@ public class User {
return t;
}), user.getUID());
}
+ return tags;
+ }
- int maxUsageCnt = tags.size() > 0 ? tags.stream().mapToInt(tag -> tag.UsageCnt).max().getAsInt() : 1;
-
- return tags.stream().map(tag -> "<a href=\"./?tag=" + URLEncoder.encode(tag.Name) +
- "\" title=\"" + tag.UsageCnt + "\" rel=\"nofollow\">" + tag.Name + "</a>")
- .collect(Collectors.joining(" "));
-
- /*
- todo:
- if (tags[i].UsageCnt > maxUsageCnt / 3 * 2) {
- ret += "<big>" + tag + "</big> ";
- } else if (tags[i].UsageCnt > maxUsageCnt / 3) {
- ret += "<small>" + tag + "</small> ";
- } else {
- ret += tag + " ";
+ public void doMyTagsJson(JdbcTemplate sql, HttpServletRequest request, HttpServletResponse response) throws IOException {
+ com.juick.User visitor = Utils.getVisitorUser(sql, request, response);
+ if (visitor != null) {
+ List<Tag> userTags = getUserTags(sql, visitor, 200);
+ JSONArray arr = new JSONArray();
+ for (Tag userTag : userTags) {
+ arr.put(userTag.Name);
}
- }*/
+ response.setContentType("text/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getOutputStream().write(arr.toString().getBytes("UTF-8"));
+ } else {
+ response.sendError(404);
+ }
}
}
diff --git a/src/main/java/com/juick/http/www/Utils.java b/src/main/java/com/juick/http/www/Utils.java
index ab721020..16428842 100644
--- a/src/main/java/com/juick/http/www/Utils.java
+++ b/src/main/java/com/juick/http/www/Utils.java
@@ -162,6 +162,7 @@ public class Utils {
}
public static String encodeHTML(String str) {
+ if (str == null) return null;
return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("'", "&apos;").replaceAll("\"", "&quot;").replaceAll("\n", "&#10;");
}
diff --git a/src/main/resources/templates/parts/page_header.html b/src/main/resources/templates/parts/page_header.html
new file mode 100644
index 00000000..63087d6c
--- /dev/null
+++ b/src/main/resources/templates/parts/page_header.html
@@ -0,0 +1,33 @@
+@args String headers, String title
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <link rel="stylesheet" href="/style.css"/>
+ <link rel="stylesheet" href="/textext/textext.core.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.arrow.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.autocomplete.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.focus.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.prompt.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.tags.css"/>
+ <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
+ <script type="text/javascript" src="/textext/textext.core.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.ajax.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.arrow.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.autocomplete.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.filter.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.focus.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.prompt.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.suggestions.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.tags.js"></script>
+ <script type="text/javascript" src="/scripts.js"></script>
+ @if (headers != null) {
+ @raw() {@headers}
+ }
+ <title>@title</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"/>
+ <link rel="icon" href="//i.juick.com/favicon.png"/>
+ <!--[if lt IE 9 & (!IEMobile 7)]>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
+ <![endif]-->
+</head>
diff --git a/src/main/resources/templates/parts/page_navigation.html b/src/main/resources/templates/parts/page_navigation.html
new file mode 100644
index 00000000..018818b0
--- /dev/null
+++ b/src/main/resources/templates/parts/page_navigation.html
@@ -0,0 +1,40 @@
+@args String search, com.juick.User visitor
+<header>
+ <div id="logo"><a href="/">Juick</a></div>
+ <nav id="global">
+ <ul>
+ <li><a href="/">Популярные</a></li>
+ <li><a href="/?show=all" rel="nofollow">Все сообщения</a></li>
+ <li><a href="/?show=photos" rel="nofollow">Фотографии</a></li>
+ </ul>
+ </nav>
+ <div id="search">
+ <form action="/"><input type="text" name="search" class="text" placeholder="Поиск"
+ @if (search !=null) {
+ value="@search"
+ }
+ /></form>
+ </div>
+ <section id="headdiv">
+ @if (visitor != null) {
+ <nav id="user">
+ <ul>
+ <li><a href="/?show=my">Моя лента</a></li>
+ <li><a href="/pm/inbox">Приватные</a></li>
+ <li><a href="/?show=discuss">Обсуждения</a></li>
+ <li><a href="/?show=recommended">Рекомендации</a></li>
+ </ul>
+ </nav>
+ <nav id="actions">
+ <ul>
+ <li><a href="/#post">Написать</a></li>
+ <li><a href="/@visitor.getUName()">@@ @visitor.getUName()</a></li>
+ <li><a href="/logout">Выйти</a></li>
+ </ul>
+ </nav>
+ } else {
+ <p>Чтобы добавлять сообщения и комментарии, <a href="#" onclick="return openDialogLogin()">представьтесь</a>.
+ </p>
+ }
+ </section>
+</header>
diff --git a/src/main/resources/templates/parts/post_form.html b/src/main/resources/templates/parts/post_form.html
new file mode 100644
index 00000000..56184f55
--- /dev/null
+++ b/src/main/resources/templates/parts/post_form.html
@@ -0,0 +1,64 @@
+<form action="/post" method="post" enctype="multipart/form-data" onsubmit="return onsubmitNewMessage()">
+ <section id="newmessage">
+ <textarea name="body" placeholder="Новое сообщение..."
+ onclick="newmessage_toggleOpen()"
+ onkeypress="postformListener(this.form,event)"></textarea>
+ <div>
+ <input type="text" class="img" name="img" placeholder="Ссылка на изображение (JPG/PNG, до 10Мб)" style="margin-bottom: 8pt"/> или <a
+ href="#" onclick="return attachMessagePhoto(this)">загрузить</a><br/>
+
+ <!--<textarea class="tags" name="tags" rows="1" id="post_tags" style="resize: none;" type="text"></textarea>-->
+ <input type="text" class="tags" name="tags" id="post_tags"/><br/>
+ <input type="submit" class="subm" value="Отправить"/>
+ </div>
+ <div id="newmessage_tags" style="padding-top: 10pt">
+ <span style="display: none; border: 1px solid black; background-color: #1277aa; color: white; padding: 2pt 2pt; font-size: 9pt; margin: 2pt 2pt">
+ sample
+ </span>
+ </div>
+ <script>
+ function newmessage_toggleOpen() {
+ $('#newmessage>div').css('display','block');
+ $('#newmessage textarea').css('min-height','70px');
+
+ $.ajax({url:"/my_tags.json"}).done(function(data) {
+ $('#post_tags').textext({
+ plugins: 'tags prompt focus autocomplete suggestions arrow',
+ tagsItems: [],
+ prompt: 'Теги (через ENTER)',
+ suggestions: data
+ });
+ var plugin = $('#post_tags').textext()[0];
+ var mtags = document.getElementById("newmessage_tags");
+ var template = mtags.firstElementChild;
+ for(var i=0; i<data.length; i++) {
+ var nextItem = template.cloneNode(true);
+ nextItem.innerText = data[i];
+ nextItem.style.display = "inline";
+ var q = function(item) {
+ item.onclick = function() {
+ window.setTimeout(function() {
+ if (plugin.tags().getTagElement(item.innerText)) {
+ plugin.tags().removeTag(item.innerText);
+ }
+ plugin.tags().addTags([item.innerText]);
+ }, 50)
+ };
+ item.onmouseover = function() {
+ item.style.fontStyle = "bold";
+ item.style.backgroundColor = "#2ba4e3";
+ };
+ item.onmouseout = function() {
+ item.style.fontStyle = "";
+ item.style.backgroundColor = template.style.backgroundColor;
+ };
+ };
+ q(nextItem);
+ mtags.appendChild(nextItem);
+ }
+ });
+ newmessage_toggleOpen = function() {}
+ }
+ </script>
+ </section>
+</form> \ No newline at end of file
diff --git a/src/main/resources/templates/parts/test.html b/src/main/resources/templates/parts/test.html
new file mode 100644
index 00000000..95e5d093
--- /dev/null
+++ b/src/main/resources/templates/parts/test.html
@@ -0,0 +1,3 @@
+
+
+this is a test!!22 \ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index 3bc3cf21..96302e3d 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -13,17 +13,25 @@
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
- <url-pattern>/scripts.js</url-pattern>
+ <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
- <url-pattern>/style.css</url-pattern>
+ <url-pattern>*.js</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>default</servlet-name>
+ <url-pattern>*.testhtml</url-pattern>
</servlet-mapping>
<mime-mapping>
<extension>js</extension>
<mime-type>application/javascript;charset=UTF-8</mime-type>
</mime-mapping>
<mime-mapping>
+ <extension>testhtml</extension>
+ <mime-type>text/html;charset=UTF-8</mime-type>
+ </mime-mapping>
+ <mime-mapping>
<extension>css</extension>
<mime-type>text/css;charset=UTF-8</mime-type>
</mime-mapping>
diff --git a/src/main/webapp/scripts.js b/src/main/webapp/scripts.js
index bb3ddc5d..811764e8 100644
--- a/src/main/webapp/scripts.js
+++ b/src/main/webapp/scripts.js
@@ -134,8 +134,9 @@ function postformListener(formEl,ev) {
function unfoldPostForm() {
if(window.location.pathname==="/" && window.location.hash==="#post") {
- $('#newmessage>div').css('display','block');
- $('#newmessage textarea').css('min-height','70px');
+ if (newmessage_toggleOpen) {
+ newmessage_toggleOpen();
+ }
$('#newmessage textarea')[0].focus();
}
}
@@ -652,6 +653,16 @@ jQuery.fn.selectText = function(){
})(jQuery);
+// listening to style changes http://stackoverflow.com/questions/2157963/is-it-possible-to-listen-to-a-style-change-event
+(function() {
+ var ev = new $.Event('style'),
+ orig = $.fn.css;
+ $.fn.css = function() {
+ $(this).trigger(ev);
+ return orig.apply(this, arguments);
+ }
+})();
+
/******************************************************************************/
$(document).ready(function() {
diff --git a/src/main/webapp/style.css b/src/main/webapp/style.css
index 69c7e2da..b247d178 100644
--- a/src/main/webapp/style.css
+++ b/src/main/webapp/style.css
@@ -48,7 +48,7 @@ body>header p { color: #000; font-size: 13pt; margin: 12px 0; text-align: center
#newmessage { background: #E5E5E0; padding: 15px; margin-bottom: 20px; }
#newmessage textarea { border: 1px solid #CCC; padding: 4px; width: 688px; resize: vertical; min-height: 14pt; height: 14pt; margin: 0 0 5px 0; }
-#newmessage input { border: 1px solid #CCC; padding: 2px 4px; margin: 5px 0; }
+#newmessage input { border: 1px solid #CCC; padding: 3px 6px 4px; /*margin: 5px 0;*/ }
#newmessage>div { display: none; }
#newmessage .img { width: 500px; }
#newmessage .tags { width: 500px; }
diff --git a/src/main/webapp/test.json b/src/main/webapp/test.json
new file mode 100644
index 00000000..6df54a41
--- /dev/null
+++ b/src/main/webapp/test.json
@@ -0,0 +1,19 @@
+[
+ "Basic",
+ "Closure",
+ "Cobol",
+ "Delphi",
+ "Erlang",
+ "Fortran",
+ "Go",
+ "Groovy",
+ "Haskel",
+ "Java",
+ "JavaScript",
+ "OCAML",
+ "PHP",
+ "Perl",
+ "Python",
+ "Ruby",
+ "Scala"
+] \ No newline at end of file
diff --git a/src/main/webapp/test.testhtml b/src/main/webapp/test.testhtml
new file mode 100644
index 00000000..e0e5092f
--- /dev/null
+++ b/src/main/webapp/test.testhtml
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Title</title>
+ <link rel="stylesheet" href="/style.css"/>
+ <link rel="stylesheet" href="/textext/textext.core.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.arrow.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.autocomplete.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.focus.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.prompt.css"/>
+ <link rel="stylesheet" href="/textext/textext.plugin.tags.css"/>
+ <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
+ <script type="text/javascript" src="/textext/textext.core.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.ajax.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.arrow.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.autocomplete.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.filter.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.focus.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.prompt.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.suggestions.js"></script>
+ <script type="text/javascript" src="/textext/textext.plugin.tags.js"></script>
+</head>
+<body>
+
+ <div>
+ xx8
+ <input type="text" class="tags" name="tags" placeholder="" id="post_tags" style="width: 600px"/><br/>
+ </div>
+ <script>
+ $('#post_tags').textext({
+ plugins : 'tags prompt focus autocomplete ajax arrow',
+ tagsItems : [],
+ prompt : 'Теги (через пробел)',
+ ajax : {
+ url : '/test.json',
+ dataType : 'json',
+ cacheResults : true
+ }
+ });
+ </script>
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/webapp/textext/textext.core.css b/src/main/webapp/textext/textext.core.css
new file mode 100644
index 00000000..ad3fec05
--- /dev/null
+++ b/src/main/webapp/textext/textext.core.css
@@ -0,0 +1,29 @@
+.text-core {
+ position: relative;
+}
+.text-core .text-wrap {
+ background: #fff;
+ position: absolute;
+}
+.text-core .text-wrap textarea,
+.text-core .text-wrap input {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-border-radius: 0px;
+ -moz-border-radius: 0px;
+ border-radius: 0px;
+ border: 1px solid #9daccc;
+ outline: none;
+ resize: none;
+ position: absolute;
+ z-index: 1;
+ background: none;
+ overflow: hidden;
+ margin: 0;
+ padding: 3px 5px 4px 5px;
+ white-space: nowrap;
+ font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
+ line-height: 13px;
+ height: auto;
+}
diff --git a/src/main/webapp/textext/textext.core.js b/src/main/webapp/textext/textext.core.js
new file mode 100644
index 00000000..39cf4d3a
--- /dev/null
+++ b/src/main/webapp/textext/textext.core.js
@@ -0,0 +1,1617 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($, undefined)
+{
+ /**
+ * TextExt is the main core class which by itself doesn't provide any functionality
+ * that is user facing, however it has the underlying mechanics to bring all the
+ * plugins together under one roof and make them work with each other or on their
+ * own.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt
+ */
+ function TextExt() {};
+
+ /**
+ * ItemManager is used to seamlessly convert between string that come from the user input to whatever
+ * the format the item data is being passed around in. It's used by all plugins that in one way or
+ * another operate with items, such as Tags, Filter, Autocomplete and Suggestions. Default implementation
+ * works with `String` type.
+ *
+ * Each instance of `TextExt` creates a new instance of default implementation of `ItemManager`
+ * unless `itemManager` option was set to another implementation.
+ *
+ * To satisfy requirements of managing items of type other than a `String`, different implementation
+ * if `ItemManager` should be supplied.
+ *
+ * If you wish to bring your own implementation, you need to create a new class and implement all the
+ * methods that `ItemManager` has. After, you need to supply your pass via the `itemManager` option during
+ * initialization like so:
+ *
+ * $('#input').textext({
+ * itemManager : CustomItemManager
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager
+ */
+ function ItemManager() {};
+
+ /**
+ * TextExtPlugin is a base class for all plugins. It provides common methods which are reused
+ * by majority of plugins.
+ *
+ * All plugins must register themselves by calling the `$.fn.textext.addPlugin(name, constructor)`
+ * function while providing plugin name and constructor. The plugin name is the same name that user
+ * will identify the plugin in the `plugins` option when initializing TextExt component and constructor
+ * function will create a new instance of the plugin. *Without registering, the core won't
+ * be able to see the plugin.*
+ *
+ * <span class="new label version">new in 1.2.0</span> You can get instance of each plugin from the core
+ * via associated function with the same name as the plugin. For example:
+ *
+ * $('#input').textext()[0].tags()
+ * $('#input').textext()[0].autocomplete()
+ * ...
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin
+ */
+ function TextExtPlugin() {};
+
+ var stringify = (JSON || {}).stringify,
+ slice = Array.prototype.slice,
+ p,
+ UNDEFINED = 'undefined',
+
+ /**
+ * TextExt provides a way to pass in the options to configure the core as well as
+ * each plugin that is being currently used. The jQuery exposed plugin `$().textext()`
+ * function takes a hash object with key/value set of options. For example:
+ *
+ * $('textarea').textext({
+ * enabled: true
+ * })
+ *
+ * There are multiple ways of passing in the options:
+ *
+ * 1. Options could be nested multiple levels deep and accessed using all lowercased, dot
+ * separated style, eg `foo.bar.world`. The manual is using this style for clarity and
+ * consistency. For example:
+ *
+ * {
+ * item: {
+ * manager: ...
+ * },
+ *
+ * html: {
+ * wrap: ...
+ * },
+ *
+ * autocomplete: {
+ * enabled: ...,
+ * dropdown: {
+ * position: ...
+ * }
+ * }
+ * }
+ *
+ * 2. Options could be specified using camel cased names in a flat key/value fashion like so:
+ *
+ * {
+ * itemManager: ...,
+ * htmlWrap: ...,
+ * autocompleteEnabled: ...,
+ * autocompleteDropdownPosition: ...
+ * }
+ *
+ * 3. Finally, options could be specified in mixed style. It's important to understand that
+ * for each dot separated name, its alternative in camel case is also checked for, eg for
+ * `foo.bar.world` it's alternatives could be `fooBarWorld`, `foo.barWorld` or `fooBar.world`,
+ * which translates to `{ foo: { bar: { world: ... } } }`, `{ fooBarWorld: ... }`,
+ * `{ foo : { barWorld : ... } }` or `{ fooBar: { world: ... } }` respectively. For example:
+ *
+ * {
+ * itemManager : ...,
+ * htmlWrap: ...,
+ * autocomplete: {
+ * enabled: ...,
+ * dropdownPosition: ...
+ * }
+ * }
+ *
+ * Mixed case is used through out the code, wherever it seems appropriate. However in the code, all option
+ * names are specified in the dot notation because it works both ways where as camel case is not
+ * being converted to its alternative dot notation.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExt.options
+ */
+
+ /**
+ * Default instance of `ItemManager` which takes `String` type as default for tags.
+ *
+ * @name item.manager
+ * @default ItemManager
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.item.manager
+ */
+ OPT_ITEM_MANAGER = 'item.manager',
+
+ /**
+ * List of plugins that should be used with the current instance of TextExt. The list could be
+ * specified as array of strings or as comma or space separated string.
+ *
+ * @name plugins
+ * @default []
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.plugins
+ */
+ OPT_PLUGINS = 'plugins',
+
+ /**
+ * TextExt allows for overriding of virtually any method that the core or any of its plugins
+ * use. This could be accomplished through the use of the `ext` option.
+ *
+ * It's possible to specifically target the core or any plugin, as well as overwrite all the
+ * desired methods everywhere.
+ *
+ * 1. Targeting the core:
+ *
+ * ext: {
+ * core: {
+ * trigger: function()
+ * {
+ * console.log('TextExt.trigger', arguments);
+ * $.fn.textext.TextExt.prototype.trigger.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * 2. Targeting individual plugins:
+ *
+ * ext: {
+ * tags: {
+ * addTags: function(tags)
+ * {
+ * console.log('TextExtTags.addTags', tags);
+ * $.fn.textext.TextExtTags.prototype.addTags.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * 3. Targeting `ItemManager` instance:
+ *
+ * ext: {
+ * itemManager: {
+ * stringToItem: function(str)
+ * {
+ * console.log('ItemManager.stringToItem', str);
+ * return $.fn.textext.ItemManager.prototype.stringToItem.apply(this, arguments);
+ * }
+ * }
+ * }
+ *
+ * 4. And finally, in edge cases you can extend everything at once:
+ *
+ * ext: {
+ * '*': {
+ * fooBar: function() {}
+ * }
+ * }
+ *
+ * @name ext
+ * @default {}
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.ext
+ */
+ OPT_EXT = 'ext',
+
+ /**
+ * HTML source that is used to generate elements necessary for the core and all other
+ * plugins to function.
+ *
+ * @name html.wrap
+ * @default '<div class="text-core"><div class="text-wrap"/></div>'
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.html.wrap
+ */
+ OPT_HTML_WRAP = 'html.wrap',
+
+ /**
+ * HTML source that is used to generate hidden input value of which will be submitted
+ * with the HTML form.
+ *
+ * @name html.hidden
+ * @default '<input type="hidden" />'
+ * @author agorbatchev
+ * @date 2011/08/20
+ * @id TextExt.options.html.hidden
+ */
+ OPT_HTML_HIDDEN = 'html.hidden',
+
+ /**
+ * Hash table of key codes and key names for which special events will be created
+ * by the core. For each entry a `[name]KeyDown`, `[name]KeyUp` and `[name]KeyPress` events
+ * will be triggered along side with `anyKeyUp` and `anyKeyDown` events for every
+ * key stroke.
+ *
+ * Here's a list of default keys:
+ *
+ * {
+ * 8 : 'backspace',
+ * 9 : 'tab',
+ * 13 : 'enter!',
+ * 27 : 'escape!',
+ * 37 : 'left',
+ * 38 : 'up!',
+ * 39 : 'right',
+ * 40 : 'down!',
+ * 46 : 'delete',
+ * 108 : 'numpadEnter'
+ * }
+ *
+ * Please note the `!` at the end of some keys. This tells the core that by default
+ * this keypress will be trapped and not passed on to the text input.
+ *
+ * @name keys
+ * @default { ... }
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.options.keys
+ */
+ OPT_KEYS = 'keys',
+
+ /**
+ * The core triggers or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExt.events
+ */
+
+ /**
+ * Core triggers `preInvalidate` event before the dimensions of padding on the text input
+ * are set.
+ *
+ * @name preInvalidate
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.preInvalidate
+ */
+ EVENT_PRE_INVALIDATE = 'preInvalidate',
+
+ /**
+ * Core triggers `postInvalidate` event after the dimensions of padding on the text input
+ * are set.
+ *
+ * @name postInvalidate
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.postInvalidate
+ */
+ EVENT_POST_INVALIDATE = 'postInvalidate',
+
+ /**
+ * Core triggers `getFormData` on every key press to collect data that will be populated
+ * into the hidden input that will be submitted with the HTML form and data that will
+ * be displayed in the input field that user is currently interacting with.
+ *
+ * All plugins that wish to affect how the data is presented or sent must react to
+ * `getFormData` and populate the data in the following format:
+ *
+ * {
+ * input : {String},
+ * form : {Object}
+ * }
+ *
+ * The data key must be a numeric weight which will be used to determine which data
+ * ends up being used. Data with the highest numerical weight gets the priority. This
+ * allows plugins to set the final data regardless of their initialization order, which
+ * otherwise would be impossible.
+ *
+ * For example, the Tags and Autocomplete plugins have to work side by side and Tags
+ * plugin must get priority on setting the data. Therefore the Tags plugin sets data
+ * with the weight 200 where as the Autocomplete plugin sets data with the weight 100.
+ *
+ * Here's an example of a typical `getFormData` handler:
+ *
+ * TextExtPlugin.prototype.onGetFormData = function(e, data, keyCode)
+ * {
+ * data[100] = self.formDataObject('input value', 'form value');
+ * };
+ *
+ * Core also reacts to the `getFormData` and updates hidden input with data which will be
+ * submitted with the HTML form.
+ *
+ * @name getFormData
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.getFormData
+ */
+ EVENT_GET_FORM_DATA = 'getFormData',
+
+ /**
+ * Core triggers and reacts to the `setFormData` event to update the actual value in the
+ * hidden input that will be submitted with the HTML form. Second argument can be value
+ * of any type and by default it will be JSON serialized with `TextExt.serializeData()`
+ * function.
+ *
+ * @name setFormData
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.events.setFormData
+ */
+ EVENT_SET_FORM_DATA = 'setFormData',
+
+ /**
+ * Core triggers and reacts to the `setInputData` event to update the actual value in the
+ * text input that user is interacting with. Second argument must be of a `String` type
+ * the value of which will be set into the text input.
+ *
+ * @name setInputData
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.events.setInputData
+ */
+ EVENT_SET_INPUT_DATA = 'setInputData',
+
+ /**
+ * Core triggers `postInit` event to let plugins run code after all plugins have been
+ * created and initialized. This is a good place to set some kind of global values before
+ * somebody gets to use them. This is not the right place to expect all plugins to finish
+ * their initialization.
+ *
+ * @name postInit
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.postInit
+ */
+ EVENT_POST_INIT = 'postInit',
+
+ /**
+ * Core triggers `ready` event after all global configuration and prepearation has been
+ * done and the TextExt component is ready for use. Event handlers should expect all
+ * values to be set and the plugins to be in the final state.
+ *
+ * @name ready
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.ready
+ */
+ EVENT_READY = 'ready',
+
+ /**
+ * Core triggers `anyKeyUp` event for every key up event triggered within the component.
+ *
+ * @name anyKeyUp
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.anyKeyUp
+ */
+
+ /**
+ * Core triggers `anyKeyDown` event for every key down event triggered within the component.
+ *
+ * @name anyKeyDown
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.anyKeyDown
+ */
+
+ /**
+ * Core triggers `[name]KeyUp` event for every key specifid in the `keys` option that is
+ * triggered within the component.
+ *
+ * @name [name]KeyUp
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.[name]KeyUp
+ */
+
+ /**
+ * Core triggers `[name]KeyDown` event for every key specified in the `keys` option that is
+ * triggered within the component.
+ *
+ * @name [name]KeyDown
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.[name]KeyDown
+ */
+
+ /**
+ * Core triggers `[name]KeyPress` event for every key specified in the `keys` option that is
+ * triggered within the component.
+ *
+ * @name [name]KeyPress
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.events.[name]KeyPress
+ */
+
+ DEFAULT_OPTS = {
+ itemManager : ItemManager,
+
+ plugins : [],
+ ext : {},
+
+ html : {
+ wrap : '<div class="text-core"><div class="text-wrap"/></div>',
+ hidden : '<input type="hidden" />'
+ },
+
+ keys : {
+ 8 : 'backspace',
+ 9 : 'tab',
+ 13 : 'enter!',
+ 27 : 'escape!',
+ 37 : 'left',
+ 38 : 'up!',
+ 39 : 'right',
+ 40 : 'down!',
+ 46 : 'delete',
+ 108 : 'numpadEnter'
+ }
+ }
+ ;
+
+ // Freak out if there's no JSON.stringify function found
+ if(!stringify)
+ throw new Error('JSON.stringify() not found');
+
+ /**
+ * Returns object property by name where name is dot-separated and object is multiple levels deep.
+ * @param target Object Source object.
+ * @param name String Dot separated property name, ie `foo.bar.world`
+ * @id core.getProperty
+ */
+ function getProperty(source, name)
+ {
+ if(typeof(name) === 'string')
+ name = name.split('.');
+
+ var fullCamelCaseName = name.join('.').replace(/\.(\w)/g, function(match, letter) { return letter.toUpperCase() }),
+ nestedName = name.shift(),
+ result
+ ;
+
+ if(typeof(result = source[fullCamelCaseName]) != UNDEFINED)
+ result = result;
+
+ else if(typeof(result = source[nestedName]) != UNDEFINED && name.length > 0)
+ result = getProperty(result, name);
+
+ // name.length here should be zero
+ return result;
+ };
+
+ /**
+ * Hooks up specified events in the scope of the current object.
+ * @author agorbatchev
+ * @date 2011/08/09
+ */
+ function hookupEvents()
+ {
+ var args = slice.apply(arguments),
+ self = this,
+ target = args.length === 1 ? self : args.shift(),
+ event
+ ;
+
+ args = args[0] || {};
+
+ function bind(event, handler)
+ {
+ target.bind(event, function()
+ {
+ // apply handler to our PLUGIN object, not the target
+ return handler.apply(self, arguments);
+ });
+ }
+
+ for(event in args)
+ bind(event, args[event]);
+ };
+
+ function formDataObject(input, form)
+ {
+ return { 'input' : input, 'form' : form };
+ };
+
+ //--------------------------------------------------------------------------------
+ // ItemManager core component
+
+ p = ItemManager.prototype;
+
+ /**
+ * Initialization method called by the core during instantiation.
+ *
+ * @signature ItemManager.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.init
+ */
+ p.init = function(core)
+ {
+ };
+
+ /**
+ * Filters out items from the list that don't match the query and returns remaining items. Default
+ * implementation checks if the item starts with the query.
+ *
+ * @signature ItemManager.filter(list, query)
+ *
+ * @param list {Array} List of items. Default implementation works with strings.
+ * @param query {String} Query string.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.filter
+ */
+ p.filter = function(list, query)
+ {
+ var result = [],
+ i, item
+ ;
+
+ for(i = 0; i < list.length; i++)
+ {
+ item = list[i];
+ if(this.itemContains(item, query))
+ result.push(item);
+ }
+
+ return result;
+ };
+
+ /**
+ * Returns `true` if specified item contains another string, `false` otherwise. In the default implementation
+ * `String.indexOf()` is used to check if item string begins with the needle string.
+ *
+ * @signature ItemManager.itemContains(item, needle)
+ *
+ * @param item {Object} Item to check. Default implementation works with strings.
+ * @param needle {String} Search string to be found within the item.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.itemContains
+ */
+ p.itemContains = function(item, needle)
+ {
+ return this.itemToString(item).toLowerCase().indexOf(needle.toLowerCase()) == 0;
+ };
+
+ /**
+ * Converts specified string to item. Because default implemenation works with string, input string
+ * is simply returned back. To use custom objects, different implementation of this method could
+ * return something like `{ name : {String} }`.
+ *
+ * @signature ItemManager.stringToItem(str)
+ *
+ * @param str {String} Input string.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.stringToItem
+ */
+ p.stringToItem = function(str)
+ {
+ return str;
+ };
+
+ /**
+ * Converts specified item to string. Because default implemenation works with string, input string
+ * is simply returned back. To use custom objects, different implementation of this method could
+ * for example return `name` field of `{ name : {String} }`.
+ *
+ * @signature ItemManager.itemToString(item)
+ *
+ * @param item {Object} Input item to be converted to string.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.itemToString
+ */
+ p.itemToString = function(item)
+ {
+ return item;
+ };
+
+ /**
+ * Returns `true` if both items are equal, `false` otherwise. Because default implemenation works with
+ * string, input items are compared as strings. To use custom objects, different implementation of this
+ * method could for example compare `name` fields of `{ name : {String} }` type object.
+ *
+ * @signature ItemManager.compareItems(item1, item2)
+ *
+ * @param item1 {Object} First item.
+ * @param item2 {Object} Second item.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id ItemManager.compareItems
+ */
+ p.compareItems = function(item1, item2)
+ {
+ return item1 == item2;
+ };
+
+ //--------------------------------------------------------------------------------
+ // TextExt core component
+
+ p = TextExt.prototype;
+
+ /**
+ * Initializes current component instance with work with the supplied text input and options.
+ *
+ * @signature TextExt.init(input, opts)
+ *
+ * @param input {HTMLElement} Text input.
+ * @param opts {Object} Options.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.init
+ */
+ p.init = function(input, opts)
+ {
+ var self = this,
+ hiddenInput,
+ itemManager,
+ container
+ ;
+
+ self._defaults = $.extend({}, DEFAULT_OPTS);
+ self._opts = opts || {};
+ self._plugins = {};
+ self._itemManager = itemManager = new (self.opts(OPT_ITEM_MANAGER))();
+ input = $(input);
+ container = $(self.opts(OPT_HTML_WRAP));
+ hiddenInput = $(self.opts(OPT_HTML_HIDDEN));
+
+ input
+ .wrap(container)
+ .keydown(function(e) { return self.onKeyDown(e) })
+ .keyup(function(e) { return self.onKeyUp(e) })
+ .data('textext', self)
+ ;
+
+ // keep references to html elements using jQuery.data() to avoid circular references
+ $(self).data({
+ 'hiddenInput' : hiddenInput,
+ 'wrapElement' : input.parents('.text-wrap').first(),
+ 'input' : input
+ });
+
+ // set the name of the hidden input to the text input's name
+ hiddenInput.attr('name', input.attr('name'));
+ // remove name attribute from the text input
+ input.attr('name', null);
+ // add hidden input to the DOM
+ hiddenInput.insertAfter(input);
+
+ $.extend(true, itemManager, self.opts(OPT_EXT + '.item.manager'));
+ $.extend(true, self, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.core'));
+
+ self.originalWidth = input.outerWidth();
+
+ self.invalidateBounds();
+
+ itemManager.init(self);
+
+ self.initPatches();
+ self.initPlugins(self.opts(OPT_PLUGINS), $.fn.textext.plugins);
+
+ self.on({
+ setFormData : self.onSetFormData,
+ getFormData : self.onGetFormData,
+ setInputData : self.onSetInputData,
+ anyKeyUp : self.onAnyKeyUp
+ });
+
+ self.trigger(EVENT_POST_INIT);
+ self.trigger(EVENT_READY);
+
+ self.getFormData(0);
+ };
+
+ /**
+ * Initialized all installed patches against current instance. The patches are initialized based on their
+ * initialization priority which is returned by each patch's `initPriority()` method. Priority
+ * is a `Number` where patches with higher value gets their `init()` method called before patches
+ * with lower priority value.
+ *
+ * This facilitates initializing of patches in certain order to insure proper dependencies
+ * regardless of which order they are loaded.
+ *
+ * By default all patches have the same priority - zero, which means they will be initialized
+ * in rorder they are loaded, that is unless `initPriority()` is overriden.
+ *
+ * @signature TextExt.initPatches()
+ *
+ * @author agorbatchev
+ * @date 2011/10/11
+ * @id TextExt.initPatches
+ */
+ p.initPatches = function()
+ {
+ var list = [],
+ source = $.fn.textext.patches,
+ name
+ ;
+
+ for(name in source)
+ list.push(name);
+
+ this.initPlugins(list, source);
+ };
+
+ /**
+ * Creates and initializes all specified plugins. The plugins are initialized based on their
+ * initialization priority which is returned by each plugin's `initPriority()` method. Priority
+ * is a `Number` where plugins with higher value gets their `init()` method called before plugins
+ * with lower priority value.
+ *
+ * This facilitates initializing of plugins in certain order to insure proper dependencies
+ * regardless of which order user enters them in the `plugins` option field.
+ *
+ * By default all plugins have the same priority - zero, which means they will be initialized
+ * in the same order as entered by the user.
+ *
+ * @signature TextExt.initPlugins(plugins)
+ *
+ * @param plugins {Array} List of plugin names to initialize.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.initPlugins
+ */
+ p.initPlugins = function(plugins, source)
+ {
+ var self = this,
+ ext, name, plugin, initList = [], i
+ ;
+
+ if(typeof(plugins) == 'string')
+ plugins = plugins.split(/\s*,\s*|\s+/g);
+
+ for(i = 0; i < plugins.length; i++)
+ {
+ name = plugins[i];
+ plugin = source[name];
+
+ if(plugin)
+ {
+ self._plugins[name] = plugin = new plugin();
+ self[name] = (function(plugin) {
+ return function(){ return plugin; }
+ })(plugin);
+ initList.push(plugin);
+ $.extend(true, plugin, self.opts(OPT_EXT + '.*'), self.opts(OPT_EXT + '.' + name));
+ }
+ }
+
+ // sort plugins based on their priority values
+ initList.sort(function(p1, p2)
+ {
+ p1 = p1.initPriority();
+ p2 = p2.initPriority();
+
+ return p1 === p2
+ ? 0
+ : p1 < p2 ? 1 : -1
+ ;
+ });
+
+ for(i = 0; i < initList.length; i++)
+ initList[i].init(self);
+ };
+
+ /**
+ * Returns true if specified plugin is was instantiated for the current instance of core.
+ *
+ * @signature TextExt.hasPlugin(name)
+ *
+ * @param name {String} Name of the plugin to check.
+ *
+ * @author agorbatchev
+ * @date 2011/12/28
+ * @id TextExt.hasPlugin
+ * @version 1.1
+ */
+ p.hasPlugin = function(name)
+ {
+ return !!this._plugins[name];
+ };
+
+ /**
+ * Allows to add multiple event handlers which will be execued in the scope of the current object.
+ *
+ * @signature TextExt.on([target], handlers)
+ *
+ * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
+ * Handler function will still be executed in the current object's scope.
+ * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.on
+ */
+ p.on = hookupEvents;
+
+ /**
+ * Binds an event handler to the input box that user interacts with.
+ *
+ * @signature TextExt.bind(event, handler)
+ *
+ * @param event {String} Event name.
+ * @param handler {Function} Event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.bind
+ */
+ p.bind = function(event, handler)
+ {
+ this.input().bind(event, handler);
+ };
+
+ /**
+ * Triggers an event on the input box that user interacts with. All core events are originated here.
+ *
+ * @signature TextExt.trigger(event, ...args)
+ *
+ * @param event {String} Name of the event to trigger.
+ * @param ...args All remaining arguments will be passed to the event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.trigger
+ */
+ p.trigger = function()
+ {
+ var args = arguments;
+ this.input().trigger(args[0], slice.call(args, 1));
+ };
+
+ /**
+ * Returns instance of `itemManager` that is used by the component.
+ *
+ * @signature TextExt.itemManager()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.itemManager
+ */
+ p.itemManager = function()
+ {
+ return this._itemManager;
+ };
+
+ /**
+ * Returns jQuery input element with which user is interacting with.
+ *
+ * @signature TextExt.input()
+ *
+ * @author agorbatchev
+ * @date 2011/08/10
+ * @id TextExt.input
+ */
+ p.input = function()
+ {
+ return $(this).data('input');
+ };
+
+ /**
+ * Returns option value for the specified option by name. If the value isn't found in the user
+ * provided options, it will try looking for default value.
+ *
+ * @signature TextExt.opts(name)
+ *
+ * @param name {String} Option name as described in the options.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.opts
+ */
+ p.opts = function(name)
+ {
+ var result = getProperty(this._opts, name);
+ return typeof(result) == 'undefined' ? getProperty(this._defaults, name) : result;
+ };
+
+ /**
+ * Returns HTML element that was created from the `html.wrap` option. This is the top level HTML
+ * container for the text input with which user is interacting with.
+ *
+ * @signature TextExt.wrapElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.wrapElement
+ */
+ p.wrapElement = function()
+ {
+ return $(this).data('wrapElement');
+ };
+
+ /**
+ * Updates container to match dimensions of the text input. Triggers `preInvalidate` and `postInvalidate`
+ * events.
+ *
+ * @signature TextExt.invalidateBounds()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.invalidateBounds
+ */
+ p.invalidateBounds = function()
+ {
+ var self = this,
+ input = self.input(),
+ wrap = self.wrapElement(),
+ container = wrap.parent(),
+ width = self.originalWidth + 'px',
+ height
+ ;
+
+ self.trigger(EVENT_PRE_INVALIDATE);
+
+ height = input.outerHeight() + 'px';
+
+ // using css() method instead of width() and height() here because they don't seem to do the right thing in jQuery 1.8.x
+ // https://github.com/alexgorbatchev/jquery-textext/issues/74
+ input.css({ 'width' : width });
+ wrap.css({ 'width' : width, 'height' : height });
+ container.css({ 'height' : height });
+
+ self.trigger(EVENT_POST_INVALIDATE);
+ };
+
+ /**
+ * Focuses user input on the text box.
+ *
+ * @signature TextExt.focusInput()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.focusInput
+ */
+ p.focusInput = function()
+ {
+ this.input()[0].focus();
+ };
+
+ /**
+ * Serializes data for to be set into the hidden input field and which will be submitted
+ * with the HTML form.
+ *
+ * By default simple JSON serialization is used. It's expected that `JSON.stringify`
+ * method would be available either through built in class in most modern browsers
+ * or through JSON2 library.
+ *
+ * @signature TextExt.serializeData(data)
+ *
+ * @param data {Object} Data to serialize.
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExt.serializeData
+ */
+ p.serializeData = stringify;
+
+ /**
+ * Returns the hidden input HTML element which will be submitted with the HTML form.
+ *
+ * @signature TextExt.hiddenInput()
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExt.hiddenInput
+ */
+ p.hiddenInput = function(value)
+ {
+ return $(this).data('hiddenInput');
+ };
+
+ /**
+ * Abstracted functionality to trigger an event and get the data with maximum weight set by all
+ * the event handlers. This functionality is used for the `getFormData` event.
+ *
+ * @signature TextExt.getWeightedEventResponse(event, args)
+ *
+ * @param event {String} Event name.
+ * @param args {Object} Argument to be passed with the event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.getWeightedEventResponse
+ */
+ p.getWeightedEventResponse = function(event, args)
+ {
+ var self = this,
+ data = {},
+ maxWeight = 0
+ ;
+
+ self.trigger(event, data, args);
+
+ for(var weight in data)
+ maxWeight = Math.max(maxWeight, weight);
+
+ return data[maxWeight];
+ };
+
+ /**
+ * Triggers the `getFormData` event to get all the plugins to return their data.
+ *
+ * After the data is returned, triggers `setFormData` and `setInputData` to update appopriate values.
+ *
+ * @signature TextExt.getFormData(keyCode)
+ *
+ * @param keyCode {Number} Key code number which has triggered this update. It's impotant to pass
+ * this value to the plugins because they might return different values based on the key that was
+ * pressed. For example, the Tags plugin returns an empty string for the `input` value if the enter
+ * key was pressed, otherwise it returns whatever is currently in the text input.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.getFormData
+ */
+ p.getFormData = function(keyCode)
+ {
+ var self = this,
+ data = self.getWeightedEventResponse(EVENT_GET_FORM_DATA, keyCode || 0)
+ ;
+
+ self.trigger(EVENT_SET_FORM_DATA , data['form']);
+ self.trigger(EVENT_SET_INPUT_DATA , data['input']);
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `anyKeyUp` event and triggers the `getFormData` to change data that will be submitted
+ * with the form. Default behaviour is that everything that is typed in will be JSON serialized, so
+ * the end result will be a JSON string.
+ *
+ * @signature TextExt.onAnyKeyUp(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.onAnyKeyUp
+ */
+ p.onAnyKeyUp = function(e, keyCode)
+ {
+ this.getFormData(keyCode);
+ };
+
+ /**
+ * Reacts to the `setInputData` event and populates the input text field that user is currently
+ * interacting with.
+ *
+ * @signature TextExt.onSetInputData(e, data)
+ *
+ * @param e {Event} jQuery event.
+ * @param data {String} Value to be set.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.onSetInputData
+ */
+ p.onSetInputData = function(e, data)
+ {
+ this.input().val(data);
+ };
+
+ /**
+ * Reacts to the `setFormData` event and populates the hidden input with will be submitted with
+ * the HTML form. The value will be serialized with `serializeData()` method.
+ *
+ * @signature TextExt.onSetFormData(e, data)
+ *
+ * @param e {Event} jQuery event.
+ * @param data {Object} Data that will be set.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExt.onSetFormData
+ */
+ p.onSetFormData = function(e, data)
+ {
+ var self = this;
+ self.hiddenInput().val(self.serializeData(data));
+ };
+
+ /**
+ * Reacts to `getFormData` event triggered by the core. At the bare minimum the core will tell
+ * itself to use the current value in the text input as the data to be submitted with the HTML
+ * form.
+ *
+ * @signature TextExt.onGetFormData(e, data)
+ *
+ * @param e {Event} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExt.onGetFormData
+ */
+ p.onGetFormData = function(e, data)
+ {
+ var val = this.input().val();
+ data[0] = formDataObject(val, val);
+ };
+
+ //--------------------------------------------------------------------------------
+ // User mouse/keyboard input
+
+ /**
+ * Triggers `[name]KeyUp` and `[name]KeyPress` for every keystroke as described in the events.
+ *
+ * @signature TextExt.onKeyUp(e)
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.onKeyUp
+ */
+
+ /**
+ * Triggers `[name]KeyDown` for every keystroke as described in the events.
+ *
+ * @signature TextExt.onKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.onKeyDown
+ */
+
+ $(['Down', 'Up']).each(function()
+ {
+ var type = this.toString();
+
+ p['onKey' + type] = function(e)
+ {
+ var self = this,
+ keyName = self.opts(OPT_KEYS)[e.keyCode],
+ defaultResult = true
+ ;
+
+ if(keyName)
+ {
+ defaultResult = keyName.substr(-1) != '!';
+ keyName = keyName.replace('!', '');
+
+ self.trigger(keyName + 'Key' + type);
+
+ // manual *KeyPress event fimplementation for the function keys like Enter, Backspace, etc.
+ if(type == 'Up' && self._lastKeyDown == e.keyCode)
+ {
+ self._lastKeyDown = null;
+ self.trigger(keyName + 'KeyPress');
+ }
+
+ if(type == 'Down')
+ self._lastKeyDown = e.keyCode;
+ }
+
+ self.trigger('anyKey' + type, e.keyCode);
+
+ return defaultResult;
+ };
+ });
+
+ //--------------------------------------------------------------------------------
+ // Plugin Base
+
+ p = TextExtPlugin.prototype;
+
+ /**
+ * Allows to add multiple event handlers which will be execued in the scope of the current object.
+ *
+ * @signature TextExt.on([target], handlers)
+ *
+ * @param target {Object} **Optional**. Target object which has traditional `bind(event, handler)` method.
+ * Handler function will still be executed in the current object's scope.
+ * @param handlers {Object} Key/value pairs of event names and handlers, eg `{ event: handler }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.on
+ */
+ p.on = hookupEvents;
+
+ /**
+ * Returns the hash object that `getFormData` triggered by the core expects.
+ *
+ * @signature TextExtPlugin.formDataObject(input, form)
+ *
+ * @param input {String} Value that will go into the text input that user is interacting with.
+ * @param form {Object} Value that will be serialized and put into the hidden that will be submitted
+ * with the HTML form.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtPlugin.formDataObject
+ */
+ p.formDataObject = formDataObject;
+
+ /**
+ * Initialization method called by the core during plugin instantiation. This method must be implemented
+ * by each plugin individually.
+ *
+ * @signature TextExtPlugin.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.init
+ */
+ p.init = function(core) { throw new Error('Not implemented') };
+
+ /**
+ * Initialization method wich should be called by the plugin during the `init()` call.
+ *
+ * @signature TextExtPlugin.baseInit(core, defaults)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ * @param defaults {Object} Default plugin options. These will be checked if desired value wasn't
+ * found in the options supplied by the user.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.baseInit
+ */
+ p.baseInit = function(core, defaults)
+ {
+ var self = this;
+
+ core._defaults = $.extend(true, core._defaults, defaults);
+ self._core = core;
+ self._timers = {};
+ };
+
+ /**
+ * Allows starting of multiple timeout calls. Each time this method is called with the same
+ * timer name, the timer is reset. This functionality is useful in cases where an action needs
+ * to occur only after a certain period of inactivity. For example, making an AJAX call after
+ * user stoped typing for 1 second.
+ *
+ * @signature TextExtPlugin.startTimer(name, delay, callback)
+ *
+ * @param name {String} Timer name.
+ * @param delay {Number} Delay in seconds.
+ * @param callback {Function} Callback function.
+ *
+ * @author agorbatchev
+ * @date 2011/08/25
+ * @id TextExtPlugin.startTimer
+ */
+ p.startTimer = function(name, delay, callback)
+ {
+ var self = this;
+
+ self.stopTimer(name);
+
+ self._timers[name] = setTimeout(
+ function()
+ {
+ delete self._timers[name];
+ callback.apply(self);
+ },
+ delay * 1000
+ );
+ };
+
+ /**
+ * Stops the timer by name without resetting it.
+ *
+ * @signature TextExtPlugin.stopTimer(name)
+ *
+ * @param name {String} Timer name.
+ *
+ * @author agorbatchev
+ * @date 2011/08/25
+ * @id TextExtPlugin.stopTimer
+ */
+ p.stopTimer = function(name)
+ {
+ clearTimeout(this._timers[name]);
+ };
+
+ /**
+ * Returns instance of the `TextExt` to which current instance of the plugin is attached to.
+ *
+ * @signature TextExtPlugin.core()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.core
+ */
+ p.core = function()
+ {
+ return this._core;
+ };
+
+ /**
+ * Shortcut to the core's `opts()` method. Returns option value.
+ *
+ * @signature TextExtPlugin.opts(name)
+ *
+ * @param name {String} Option name as described in the options.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.opts
+ */
+ p.opts = function(name)
+ {
+ return this.core().opts(name);
+ };
+
+ /**
+ * Shortcut to the core's `itemManager()` method. Returns instance of the `ItemManger` that is
+ * currently in use.
+ *
+ * @signature TextExtPlugin.itemManager()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.itemManager
+ */
+ p.itemManager = function()
+ {
+ return this.core().itemManager();
+ };
+
+ /**
+ * Shortcut to the core's `input()` method. Returns instance of the HTML element that represents
+ * current text input.
+ *
+ * @signature TextExtPlugin.input()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.input
+ */
+ p.input = function()
+ {
+ return this.core().input();
+ };
+
+ /**
+ * Shortcut to the commonly used `this.input().val()` call to get or set value of the text input.
+ *
+ * @signature TextExtPlugin.val(value)
+ *
+ * @param value {String} Optional value. If specified, the value will be set, otherwise it will be
+ * returned.
+ *
+ * @author agorbatchev
+ * @date 2011/08/20
+ * @id TextExtPlugin.val
+ */
+ p.val = function(value)
+ {
+ var input = this.input();
+
+ if(typeof(value) === UNDEFINED)
+ return input.val();
+ else
+ input.val(value);
+ };
+
+ /**
+ * Shortcut to the core's `trigger()` method. Triggers specified event with arguments on the
+ * component core.
+ *
+ * @signature TextExtPlugin.trigger(event, ...args)
+ *
+ * @param event {String} Name of the event to trigger.
+ * @param ...args All remaining arguments will be passed to the event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtPlugin.trigger
+ */
+ p.trigger = function()
+ {
+ var core = this.core();
+ core.trigger.apply(core, arguments);
+ };
+
+ /**
+ * Shortcut to the core's `bind()` method. Binds specified handler to the event.
+ *
+ * @signature TextExtPlugin.bind(event, handler)
+ *
+ * @param event {String} Event name.
+ * @param handler {Function} Event handler.
+ *
+ * @author agorbatchev
+ * @date 2011/08/20
+ * @id TextExtPlugin.bind
+ */
+ p.bind = function(event, handler)
+ {
+ this.core().bind(event, handler);
+ };
+
+ /**
+ * Returns initialization priority for this plugin. If current plugin depends upon some other plugin
+ * to be initialized before or after, priority needs to be adjusted accordingly. Plugins with higher
+ * priority initialize before plugins with lower priority.
+ *
+ * Default initialization priority is `0`.
+ *
+ * @signature TextExtPlugin.initPriority()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtPlugin.initPriority
+ */
+ p.initPriority = function()
+ {
+ return 0;
+ };
+
+ //--------------------------------------------------------------------------------
+ // jQuery Integration
+
+ /**
+ * TextExt integrates as a jQuery plugin available through the `$(selector).textext(opts)` call. If
+ * `opts` argument is passed, then a new instance of `TextExt` will be created for all the inputs
+ * that match the `selector`. If `opts` wasn't passed and TextExt was already intantiated for
+ * inputs that match the `selector`, array of `TextExt` instances will be returned instead.
+ *
+ * // will create a new instance of `TextExt` for all elements that match `.sample`
+ * $('.sample').textext({ ... });
+ *
+ * // will return array of all `TextExt` instances
+ * var list = $('.sample').textext();
+ *
+ * The following properties are also exposed through the jQuery `$.fn.textext`:
+ *
+ * * `TextExt` -- `TextExt` class.
+ * * `TextExtPlugin` -- `TextExtPlugin` class.
+ * * `ItemManager` -- `ItemManager` class.
+ * * `plugins` -- Key/value table of all registered plugins.
+ * * `addPlugin(name, constructor)` -- All plugins should register themselves using this function.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExt.jquery
+ */
+
+ var cssInjected = false;
+
+ var textext = $.fn.textext = function(opts)
+ {
+ var css;
+
+ if(!cssInjected && (css = $.fn.textext.css) != null)
+ {
+ $('head').append('<style>' + css + '</style>');
+ cssInjected = true;
+ }
+
+ return this.map(function()
+ {
+ var self = $(this);
+
+ if(opts == null)
+ return self.data('textext');
+
+ var instance = new TextExt();
+
+ instance.init(self, opts);
+ self.data('textext', instance);
+
+ return instance.input()[0];
+ });
+ };
+
+ /**
+ * This static function registers a new plugin which makes it available through the `plugins` option
+ * to the end user. The name specified here is the name the end user would put in the `plugins` option
+ * to add this plugin to a new instance of TextExt.
+ *
+ * @signature $.fn.textext.addPlugin(name, constructor)
+ *
+ * @param name {String} Name of the plugin.
+ * @param constructor {Function} Plugin constructor.
+ *
+ * @author agorbatchev
+ * @date 2011/10/11
+ * @id TextExt.addPlugin
+ */
+ textext.addPlugin = function(name, constructor)
+ {
+ textext.plugins[name] = constructor;
+ constructor.prototype = new textext.TextExtPlugin();
+ };
+
+ /**
+ * This static function registers a new patch which is added to each instance of TextExt. If you are
+ * adding a new patch, make sure to call this method.
+ *
+ * @signature $.fn.textext.addPatch(name, constructor)
+ *
+ * @param name {String} Name of the patch.
+ * @param constructor {Function} Patch constructor.
+ *
+ * @author agorbatchev
+ * @date 2011/10/11
+ * @id TextExt.addPatch
+ */
+ textext.addPatch = function(name, constructor)
+ {
+ textext.patches[name] = constructor;
+ constructor.prototype = new textext.TextExtPlugin();
+ };
+
+ textext.TextExt = TextExt;
+ textext.TextExtPlugin = TextExtPlugin;
+ textext.ItemManager = ItemManager;
+ textext.plugins = {};
+ textext.patches = {};
+})(jQuery);
+
+(function($)
+{
+ function TextExtIE9Patches() {};
+
+ $.fn.textext.TextExtIE9Patches = TextExtIE9Patches;
+ $.fn.textext.addPatch('ie9',TextExtIE9Patches);
+
+ var p = TextExtIE9Patches.prototype;
+
+ p.init = function(core)
+ {
+ if(navigator.userAgent.indexOf('MSIE 9') == -1)
+ return;
+
+ var self = this;
+
+ core.on({ postInvalidate : self.onPostInvalidate });
+ };
+
+ p.onPostInvalidate = function()
+ {
+ var self = this,
+ input = self.input(),
+ val = input.val()
+ ;
+
+ // agorbatchev :: IE9 doesn't seem to update the padding if box-sizing is on until the
+ // text box value changes, so forcing this change seems to do the trick of updating
+ // IE's padding visually.
+ input.val(Math.random());
+ input.val(val);
+ };
+})(jQuery);
+
diff --git a/src/main/webapp/textext/textext.plugin.ajax.js b/src/main/webapp/textext/textext.plugin.ajax.js
new file mode 100644
index 00000000..073f46ab
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.ajax.js
@@ -0,0 +1,354 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * AJAX plugin is very useful if you want to load list of items from a data point and pass it
+ * to the Autocomplete or Filter plugins.
+ *
+ * Because it meant to be as a helper method for either Autocomplete or Filter plugin, without
+ * either of these two present AJAX plugin won't do anything.
+ *
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax
+ */
+ function TextExtAjax() {};
+
+ $.fn.textext.TextExtAjax = TextExtAjax;
+ $.fn.textext.addPlugin('ajax', TextExtAjax);
+
+ var p = TextExtAjax.prototype,
+
+ /**
+ * AJAX plugin options are grouped under `ajax` when passed to the `$().textext()` function. Be
+ * mindful that the whole `ajax` object is also passed to jQuery `$.ajax` call which means that
+ * you can change all jQuery options as well. Please refer to the jQuery documentation on how
+ * to set url and all other parameters. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'ajax',
+ * ajax: {
+ * url: 'http://...'
+ * }
+ * })
+ *
+ * **Important**: Because it's necessary to pass options to `jQuery.ajax()` in a single object,
+ * all jQuery related AJAX options like `url`, `dataType`, etc **must** be within the `ajax` object.
+ * This is the exception to general rule that TextExt options can be specified in dot or camel case
+ * notation.
+ *
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax.options
+ */
+
+ /**
+ * By default, when user starts typing into the text input, AJAX plugin will start making requests
+ * to the `url` that you have specified and will pass whatever user has typed so far as a parameter
+ * named `q`, eg `?q=foo`.
+ *
+ * If you wish to change this behaviour, you can pass a function as a value for this option which
+ * takes one argument (the user input) and should return a key/value object that will be converted
+ * to the request parameters. For example:
+ *
+ * 'dataCallback' : function(query)
+ * {
+ * return { 'search' : query };
+ * }
+ *
+ * @name ajax.data.callback
+ * @default null
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax.options.data.callback
+ */
+ OPT_DATA_CALLBACK = 'ajax.data.callback',
+
+ /**
+ * By default, the server end point is constantly being reloaded whenever user changes the value
+ * in the text input. If you'd rather have the client do result filtering, you can return all
+ * possible results from the server and cache them on the client by setting this option to `true`.
+ *
+ * In such a case, only one call to the server will be made and filtering will be performed on
+ * the client side using `ItemManager` attached to the core.
+ *
+ * @name ajax.data.results
+ * @default false
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax.options.cache.results
+ */
+ OPT_CACHE_RESULTS = 'ajax.cache.results',
+
+ /**
+ * The loading message delay is set in seconds and will specify how long it would take before
+ * user sees the message. If you don't want user to ever see this message, set the option value
+ * to `Number.MAX_VALUE`.
+ *
+ * @name ajax.loading.delay
+ * @default 0.5
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax.options.loading.delay
+ */
+ OPT_LOADING_DELAY = 'ajax.loading.delay',
+
+ /**
+ * Whenever an AJAX request is made and the server takes more than the number of seconds specified
+ * in `ajax.loading.delay` to respond, the message specified in this option will appear in the drop
+ * down.
+ *
+ * @name ajax.loading.message
+ * @default "Loading..."
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.options.loading.message
+ */
+ OPT_LOADING_MESSAGE = 'ajax.loading.message',
+
+ /**
+ * When user is typing in or otherwise changing the value of the text input, it's undesirable to make
+ * an AJAX request for every keystroke. Instead it's more conservative to send a request every number
+ * of seconds while user is typing the value. This number of seconds is specified by the `ajax.type.delay`
+ * option.
+ *
+ * @name ajax.type.delay
+ * @default 0.5
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.options.type.delay
+ */
+ OPT_TYPE_DELAY = 'ajax.type.delay',
+
+ /**
+ * AJAX plugin dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.events
+ */
+
+ /**
+ * AJAX plugin reacts to the `getSuggestions` event dispatched by the Autocomplete plugin.
+ *
+ * @name getSuggestions
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.events.getSuggestions
+ */
+
+ /**
+ * In the event of successful AJAX request, the AJAX coponent dispatches the `setSuggestions`
+ * event meant to be recieved by the Autocomplete plugin.
+ *
+ * @name setSuggestions
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.events.setSuggestions
+ */
+ EVENT_SET_SUGGESTION = 'setSuggestions',
+
+ /**
+ * AJAX plugin dispatches the `showDropdown` event which Autocomplete plugin is expecting.
+ * This is used to temporarily show the loading message if the AJAX request is taking longer
+ * than expected.
+ *
+ * @name showDropdown
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.events.showDropdown
+ */
+ EVENT_SHOW_DROPDOWN = 'showDropdown',
+
+ TIMER_LOADING = 'loading',
+
+ DEFAULT_OPTS = {
+ ajax : {
+ typeDelay : 0.5,
+ loadingMessage : 'Loading...',
+ loadingDelay : 0.5,
+ cacheResults : false,
+ dataCallback : null
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtAjax.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAjax.init
+ */
+ p.init = function(core)
+ {
+ var self = this;
+
+ self.baseInit(core, DEFAULT_OPTS);
+
+ self.on({
+ getSuggestions : self.onGetSuggestions
+ });
+
+ self._suggestions = null;
+ };
+
+ /**
+ * Performas an async AJAX with specified options.
+ *
+ * @signature TextExtAjax.load(query)
+ *
+ * @param query {String} Value that user has typed into the text area which is
+ * presumably the query.
+ *
+ * @author agorbatchev
+ * @date 2011/08/14
+ * @id TextExtAjax.load
+ */
+ p.load = function(query)
+ {
+ var self = this,
+ dataCallback = self.opts(OPT_DATA_CALLBACK) || function(query) { return { q : query } },
+ opts
+ ;
+
+ opts = $.extend(true,
+ {
+ data : dataCallback(query),
+ success : function(data) { self.onComplete(data, query) },
+ error : function(jqXHR, message) { console.error(message, query) }
+ },
+ self.opts('ajax')
+ );
+
+ $.ajax(opts);
+ };
+
+ /**
+ * Successful call AJAX handler. Takes the data that came back from AJAX and the
+ * original query that was used to make the call.
+ *
+ * @signature TextExtAjax.onComplete(data, query)
+ *
+ * @param data {Object} Data loaded from the server, should be an Array of strings
+ * by default or whatever data structure your custom `ItemManager` implements.
+ *
+ * @param query {String} Query string, ie whatever user has typed in.
+ *
+ * @author agorbatchev
+ * @date 2011/08/14
+ * @id TextExtAjax.onComplete
+ */
+ p.onComplete = function(data, query)
+ {
+ var self = this,
+ result = data
+ ;
+
+ self.dontShowLoading();
+
+ // If results are expected to be cached, then we store the original
+ // data set and return the filtered one based on the original query.
+ // That means we do filtering on the client side, instead of the
+ // server side.
+ if(self.opts(OPT_CACHE_RESULTS) == true)
+ {
+ self._suggestions = data;
+ result = self.itemManager().filter(data, query);
+ }
+
+ self.trigger(EVENT_SET_SUGGESTION, { result : result });
+ };
+
+ /**
+ * If show loading message timer was started, calling this function disables it,
+ * otherwise nothing else happens.
+ *
+ * @signature TextExtAjax.dontShowLoading()
+ *
+ * @author agorbatchev
+ * @date 2011/08/16
+ * @id TextExtAjax.dontShowLoading
+ */
+ p.dontShowLoading = function()
+ {
+ this.stopTimer(TIMER_LOADING);
+ };
+
+ /**
+ * Shows message specified in `ajax.loading.message` if loading data takes more than
+ * number of seconds specified in `ajax.loading.delay`.
+ *
+ * @signature TextExtAjax.showLoading()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TextExtAjax.showLoading
+ */
+ p.showLoading = function()
+ {
+ var self = this;
+
+ self.dontShowLoading();
+ self.startTimer(
+ TIMER_LOADING,
+ self.opts(OPT_LOADING_DELAY),
+ function()
+ {
+ self.trigger(EVENT_SHOW_DROPDOWN, function(autocomplete)
+ {
+ autocomplete.clearItems();
+ var node = autocomplete.addDropdownItem(self.opts(OPT_LOADING_MESSAGE));
+ node.addClass('text-loading');
+ });
+ }
+ );
+ };
+
+ /**
+ * Reacts to the `getSuggestions` event and begin loading suggestions. If
+ * `ajax.cache.results` is specified, all calls after the first one will use
+ * cached data and filter it with the `core.itemManager.filter()`.
+ *
+ * @signature TextExtAjax.onGetSuggestions(e, data)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Data structure passed with the `getSuggestions` event
+ * which contains the user query, eg `{ query : "..." }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TextExtAjax.onGetSuggestions
+ */
+ p.onGetSuggestions = function(e, data)
+ {
+ var self = this,
+ suggestions = self._suggestions,
+ query = (data || {}).query || ''
+ ;
+
+ if(suggestions && self.opts(OPT_CACHE_RESULTS) === true)
+ return self.onComplete(suggestions, query);
+
+ self.startTimer(
+ 'ajax',
+ self.opts(OPT_TYPE_DELAY),
+ function()
+ {
+ self.showLoading();
+ self.load(query);
+ }
+ );
+ };
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.arrow.css b/src/main/webapp/textext/textext.plugin.arrow.css
new file mode 100644
index 00000000..f5cea3dd
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.arrow.css
@@ -0,0 +1,13 @@
+.text-core .text-wrap .text-arrow {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 22px;
+ height: 22px;
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAOAQMAAADHWqTrAAAAA3NCSVQICAjb4U/gAAAABlBMVEX///8yXJnt8Ns4AAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1MzmNZGAwAAABpJREFUCJljYEAF/xsY6hkY7BgYZBgYOFBkADkdAmFDagYFAAAAAElFTkSuQmCC") 50% 50% no-repeat;
+ cursor: pointer;
+ z-index: 2;
+}
diff --git a/src/main/webapp/textext/textext.plugin.arrow.js b/src/main/webapp/textext/textext.plugin.arrow.js
new file mode 100644
index 00000000..0531acb9
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.arrow.js
@@ -0,0 +1,106 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Displays a dropdown style arrow button. The `TextExtArrow` works together with the
+ * `TextExtAutocomplete` plugin and whenever clicked tells the autocomplete plugin to
+ * display its suggestions.
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtArrow
+ */
+ function TextExtArrow() {};
+
+ $.fn.textext.TextExtArrow = TextExtArrow;
+ $.fn.textext.addPlugin('arrow', TextExtArrow);
+
+ var p = TextExtArrow.prototype,
+ /**
+ * Arrow plugin only has one option and that is its HTML template. It could be
+ * changed when passed to the `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'arrow',
+ * html: {
+ * arrow: "<span/>"
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtArrow.options
+ */
+
+ /**
+ * HTML source that is used to generate markup required for the arrow.
+ *
+ * @name html.arrow
+ * @default '<div class="text-arrow"/>'
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtArrow.options.html.arrow
+ */
+ OPT_HTML_ARROW = 'html.arrow',
+
+ DEFAULT_OPTS = {
+ html : {
+ arrow : '<div class="text-arrow"/>'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtArrow.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtArrow.init
+ */
+ p.init = function(core)
+ {
+ var self = this,
+ arrow
+ ;
+
+ self.baseInit(core, DEFAULT_OPTS);
+
+ self._arrow = arrow = $(self.opts(OPT_HTML_ARROW));
+ self.core().wrapElement().append(arrow);
+ arrow.bind('click', function(e) { self.onArrowClick(e); });
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `click` event whenever user clicks the arrow.
+ *
+ * @signature TextExtArrow.onArrowClick(e)
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtArrow.onArrowClick
+ */
+ p.onArrowClick = function(e)
+ {
+ this.trigger('toggleDropdown');
+ this.core().focusInput();
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.autocomplete.css b/src/main/webapp/textext/textext.plugin.autocomplete.css
new file mode 100644
index 00000000..a72fcbde
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.autocomplete.css
@@ -0,0 +1,35 @@
+.text-core .text-wrap .text-dropdown {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ padding: 0;
+ position: absolute;
+ z-index: 3;
+ background: #fff;
+ border: 1px solid #9daccc;
+ width: 100%;
+ max-height: 100px;
+ padding: 1px;
+ font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
+ display: none;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+.text-core .text-wrap .text-dropdown.text-position-below {
+ margin-top: 1px;
+}
+.text-core .text-wrap .text-dropdown.text-position-above {
+ margin-bottom: 1px;
+}
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion {
+ padding: 3px 5px;
+ cursor: pointer;
+}
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion em {
+ font-style: normal;
+ text-decoration: underline;
+}
+.text-core .text-wrap .text-dropdown .text-list .text-suggestion.text-selected {
+ color: #fff;
+ background: #6d84b4;
+}
diff --git a/src/main/webapp/textext/textext.plugin.autocomplete.js b/src/main/webapp/textext/textext.plugin.autocomplete.js
new file mode 100644
index 00000000..fd493e4f
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.autocomplete.js
@@ -0,0 +1,1110 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Autocomplete plugin brings the classic autocomplete functionality to the TextExt ecosystem.
+ * The gist of functionality is when user starts typing in, for example a term or a tag, a
+ * dropdown would be presented with possible suggestions to complete the input quicker.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete
+ */
+ function TextExtAutocomplete() {};
+
+ $.fn.textext.TextExtAutocomplete = TextExtAutocomplete;
+ $.fn.textext.addPlugin('autocomplete', TextExtAutocomplete);
+
+ var p = TextExtAutocomplete.prototype,
+
+ CSS_DOT = '.',
+ CSS_SELECTED = 'text-selected',
+ CSS_DOT_SELECTED = CSS_DOT + CSS_SELECTED,
+ CSS_SUGGESTION = 'text-suggestion',
+ CSS_DOT_SUGGESTION = CSS_DOT + CSS_SUGGESTION,
+ CSS_LABEL = 'text-label',
+ CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
+
+ /**
+ * Autocomplete plugin options are grouped under `autocomplete` when passed to the
+ * `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'autocomplete',
+ * autocomplete: {
+ * dropdownPosition: 'above'
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.options
+ */
+
+ /**
+ * This is a toggle switch to enable or disable the Autucomplete plugin. The value is checked
+ * each time at the top level which allows you to toggle this setting on the fly.
+ *
+ * @name autocomplete.enabled
+ * @default true
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.options.autocomplete.enabled
+ */
+ OPT_ENABLED = 'autocomplete.enabled',
+
+ /**
+ * This option allows to specify position of the dropdown. The two possible values
+ * are `above` and `below`.
+ *
+ * @name autocomplete.dropdown.position
+ * @default "below"
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.options.autocomplete.dropdown.position
+ */
+ OPT_POSITION = 'autocomplete.dropdown.position',
+
+ /**
+ * This option allows to specify maximum height of the dropdown. Value is taken directly, so
+ * if desired height is 200 pixels, value must be `200px`.
+ *
+ * @name autocomplete.dropdown.maxHeight
+ * @default "100px"
+ * @author agorbatchev
+ * @date 2011/12/29
+ * @id TextExtAutocomplete.options.autocomplete.dropdown.maxHeight
+ * @version 1.1
+ */
+ OPT_MAX_HEIGHT = 'autocomplete.dropdown.maxHeight',
+
+ /**
+ * This option allows to override how a suggestion item is rendered. The value should be
+ * a function, the first argument of which is suggestion to be rendered and `this` context
+ * is the current instance of `TextExtAutocomplete`.
+ *
+ * [Click here](/manual/examples/autocomplete-with-custom-render.html) to see a demo.
+ *
+ * For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'autocomplete',
+ * autocomplete: {
+ * render: function(suggestion)
+ * {
+ * return '<b>' + suggestion + '</b>';
+ * }
+ * }
+ * })
+ *
+ * @name autocomplete.render
+ * @default null
+ * @author agorbatchev
+ * @date 2011/12/23
+ * @id TextExtAutocomplete.options.autocomplete.render
+ * @version 1.1
+ */
+ OPT_RENDER = 'autocomplete.render',
+
+ /**
+ * HTML source that is used to generate the dropdown.
+ *
+ * @name html.dropdown
+ * @default '<div class="text-dropdown"><div class="text-list"/></div>'
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.options.html.dropdown
+ */
+ OPT_HTML_DROPDOWN = 'html.dropdown',
+
+ /**
+ * HTML source that is used to generate each suggestion.
+ *
+ * @name html.suggestion
+ * @default '<div class="text-suggestion"><span class="text-label"/></div>'
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.options.html.suggestion
+ */
+ OPT_HTML_SUGGESTION = 'html.suggestion',
+
+ /**
+ * Autocomplete plugin triggers or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.events
+ */
+
+ /**
+ * Autocomplete plugin triggers and reacts to the `hideDropdown` to hide the dropdown if it's
+ * already visible.
+ *
+ * @name hideDropdown
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.events.hideDropdown
+ */
+ EVENT_HIDE_DROPDOWN = 'hideDropdown',
+
+ /**
+ * Autocomplete plugin triggers and reacts to the `showDropdown` to show the dropdown if it's
+ * not already visible.
+ *
+ * It's possible to pass a render callback function which will be called instead of the
+ * default `TextExtAutocomplete.renderSuggestions()`.
+ *
+ * Here's how another plugin should trigger this event with the optional render callback:
+ *
+ * this.trigger('showDropdown', function(autocomplete)
+ * {
+ * autocomplete.clearItems();
+ * var node = autocomplete.addDropdownItem('<b>Item</b>');
+ * node.addClass('new-look');
+ * });
+ *
+ * @name showDropdown
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.events.showDropdown
+ */
+ EVENT_SHOW_DROPDOWN = 'showDropdown',
+
+ /**
+ * Autocomplete plugin reacts to the `setSuggestions` event triggered by other plugins which
+ * wish to populate the suggestion items. Suggestions should be passed as event argument in the
+ * following format: `{ data : [ ... ] }`.
+ *
+ * Here's how another plugin should trigger this event:
+ *
+ * this.trigger('setSuggestions', { data : [ "item1", "item2" ] });
+ *
+ * @name setSuggestions
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.events.setSuggestions
+ */
+
+ /**
+ * Autocomplete plugin triggers the `getSuggestions` event and expects to get results by listening for
+ * the `setSuggestions` event.
+ *
+ * @name getSuggestions
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.events.getSuggestions
+ */
+ EVENT_GET_SUGGESTIONS = 'getSuggestions',
+
+ /**
+ * Autocomplete plugin triggers `getFormData` event with the current suggestion so that the the core
+ * will be updated with serialized data to be submitted with the HTML form.
+ *
+ * @name getFormData
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtAutocomplete.events.getFormData
+ */
+ EVENT_GET_FORM_DATA = 'getFormData',
+
+ /**
+ * Autocomplete plugin reacts to `toggleDropdown` event and either shows or hides the dropdown
+ * depending if it's currently hidden or visible.
+ *
+ * @name toggleDropdown
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtAutocomplete.events.toggleDropdown
+ * @version 1.1
+ */
+ EVENT_TOGGLE_DROPDOWN = 'toggleDropdown',
+
+ POSITION_ABOVE = 'above',
+ POSITION_BELOW = 'below',
+
+ DATA_MOUSEDOWN_ON_AUTOCOMPLETE = 'mousedownOnAutocomplete',
+
+ DEFAULT_OPTS = {
+ autocomplete : {
+ enabled : true,
+ dropdown : {
+ position : POSITION_BELOW,
+ maxHeight : '100px'
+ }
+ },
+
+ html : {
+ dropdown : '<div class="text-dropdown"><div class="text-list"/></div>',
+ suggestion : '<div class="text-suggestion"><span class="text-label"/></div>'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtAutocomplete.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.init
+ */
+ p.init = function(core)
+ {
+ var self = this;
+
+ self.baseInit(core, DEFAULT_OPTS);
+
+ var input = self.input(),
+ container
+ ;
+
+ if(self.opts(OPT_ENABLED) === true)
+ {
+ self.on({
+ blur : self.onBlur,
+ anyKeyUp : self.onAnyKeyUp,
+ deleteKeyUp : self.onAnyKeyUp,
+ backspaceKeyPress : self.onBackspaceKeyPress,
+ enterKeyPress : self.onEnterKeyPress,
+ escapeKeyPress : self.onEscapeKeyPress,
+ setSuggestions : self.onSetSuggestions,
+ showDropdown : self.onShowDropdown,
+ hideDropdown : self.onHideDropdown,
+ toggleDropdown : self.onToggleDropdown,
+ postInvalidate : self.positionDropdown,
+ getFormData : self.onGetFormData,
+
+ // using keyDown for up/down keys so that repeat events are
+ // captured and user can scroll up/down by holding the keys
+ downKeyDown : self.onDownKeyDown,
+ upKeyDown : self.onUpKeyDown
+ });
+
+ container = $(self.opts(OPT_HTML_DROPDOWN));
+ container.insertAfter(input);
+
+ self.on(container, {
+ mouseover : self.onMouseOver,
+ mousedown : self.onMouseDown,
+ click : self.onClick
+ });
+
+ container
+ .css('maxHeight', self.opts(OPT_MAX_HEIGHT))
+ .addClass('text-position-' + self.opts(OPT_POSITION))
+ ;
+
+ $(self).data('container', container);
+
+ $(document.body).click(function(e)
+ {
+ if (self.isDropdownVisible() && !self.withinWrapElement(e.target))
+ self.trigger(EVENT_HIDE_DROPDOWN);
+ });
+
+ self.positionDropdown();
+ }
+ };
+
+ /**
+ * Returns top level dropdown container HTML element.
+ *
+ * @signature TextExtAutocomplete.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TextExtAutocomplete.containerElement
+ */
+ p.containerElement = function()
+ {
+ return $(this).data('container');
+ };
+
+ //--------------------------------------------------------------------------------
+ // User mouse/keyboard input
+
+ /**
+ * Reacts to the `mouseOver` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onMouseOver(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onMouseOver
+ */
+ p.onMouseOver = function(e)
+ {
+ var self = this,
+ target = $(e.target)
+ ;
+
+ if(target.is(CSS_DOT_SUGGESTION))
+ {
+ self.clearSelected();
+ target.addClass(CSS_SELECTED);
+ }
+ };
+
+ /**
+ * Reacts to the `mouseDown` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onMouseDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author adamayres
+ * @date 2012/01/13
+ * @id TextExtAutocomplete.onMouseDown
+ */
+ p.onMouseDown = function(e)
+ {
+ this.containerElement().data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE, true);
+ };
+
+ /**
+ * Reacts to the `click` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onClick(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onClick
+ */
+ p.onClick = function(e)
+ {
+ var self = this,
+ target = $(e.target)
+ ;
+
+ if(target.is(CSS_DOT_SUGGESTION) || target.is(CSS_DOT_LABEL))
+ self.trigger('enterKeyPress');
+
+ if (self.core().hasPlugin('tags'))
+ self.val('');
+ };
+
+ /**
+ * Reacts to the `blur` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onBlur
+ */
+ p.onBlur = function(e)
+ {
+ var self = this,
+ container = self.containerElement(),
+ isBlurByMousedown = container.data(DATA_MOUSEDOWN_ON_AUTOCOMPLETE) === true
+ ;
+
+ // only trigger a close event if the blur event was
+ // not triggered by a mousedown event on the autocomplete
+ // otherwise set focus back back on the input
+ if(self.isDropdownVisible())
+ isBlurByMousedown ? self.core().focusInput() : self.trigger(EVENT_HIDE_DROPDOWN);
+
+ container.removeData(DATA_MOUSEDOWN_ON_AUTOCOMPLETE);
+ };
+
+ /**
+ * Reacts to the `backspaceKeyPress` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onBackspaceKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onBackspaceKeyPress
+ */
+ p.onBackspaceKeyPress = function(e)
+ {
+ var self = this,
+ isEmpty = self.val().length > 0
+ ;
+
+ if(isEmpty || self.isDropdownVisible())
+ self.getSuggestions();
+ };
+
+ /**
+ * Reacts to the `anyKeyUp` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onAnyKeyUp(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onAnyKeyUp
+ */
+ p.onAnyKeyUp = function(e, keyCode)
+ {
+ var self = this,
+ isFunctionKey = self.opts('keys.' + keyCode) != null
+ ;
+
+ if(self.val().length > 0 && !isFunctionKey)
+ self.getSuggestions();
+ };
+
+ /**
+ * Reacts to the `downKeyDown` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onDownKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onDownKeyDown
+ */
+ p.onDownKeyDown = function(e)
+ {
+ var self = this;
+
+ self.isDropdownVisible()
+ ? self.toggleNextSuggestion()
+ : self.getSuggestions()
+ ;
+ };
+
+ /**
+ * Reacts to the `upKeyDown` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onUpKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onUpKeyDown
+ */
+ p.onUpKeyDown = function(e)
+ {
+ this.togglePreviousSuggestion();
+ };
+
+ /**
+ * Reacts to the `enterKeyPress` event triggered by the TextExt core.
+ *
+ * @signature TextExtAutocomplete.onEnterKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onEnterKeyPress
+ */
+ p.onEnterKeyPress = function(e)
+ {
+ var self = this;
+
+ if(self.isDropdownVisible())
+ self.selectFromDropdown();
+ };
+
+ /**
+ * Reacts to the `escapeKeyPress` event triggered by the TextExt core. Hides the dropdown
+ * if it's currently visible.
+ *
+ * @signature TextExtAutocomplete.onEscapeKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onEscapeKeyPress
+ */
+ p.onEscapeKeyPress = function(e)
+ {
+ var self = this;
+
+ if(self.isDropdownVisible())
+ self.trigger(EVENT_HIDE_DROPDOWN);
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * Positions dropdown either below or above the input based on the `autocomplete.dropdown.position`
+ * option specified, which could be either `above` or `below`.
+ *
+ * @signature TextExtAutocomplete.positionDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TextExtAutocomplete.positionDropdown
+ */
+ p.positionDropdown = function()
+ {
+ var self = this,
+ container = self.containerElement(),
+ direction = self.opts(OPT_POSITION),
+ height = self.core().wrapElement().outerHeight(),
+ css = {}
+ ;
+
+ css[direction === POSITION_ABOVE ? 'bottom' : 'top'] = height + 'px';
+ container.css(css);
+ };
+
+ /**
+ * Returns list of all the suggestion HTML elements in the dropdown.
+ *
+ * @signature TextExtAutocomplete.suggestionElements()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.suggestionElements
+ */
+ p.suggestionElements = function()
+ {
+ return this.containerElement().find(CSS_DOT_SUGGESTION);
+ };
+
+
+ /**
+ * Highlights specified suggestion as selected in the dropdown.
+ *
+ * @signature TextExtAutocomplete.setSelectedSuggestion(suggestion)
+ *
+ * @param suggestion {Object} Suggestion object. With the default `ItemManager` this
+ * is expected to be a string, anything else with custom implementations.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.setSelectedSuggestion
+ */
+ p.setSelectedSuggestion = function(suggestion)
+ {
+ if(!suggestion)
+ return;
+
+ var self = this,
+ all = self.suggestionElements(),
+ target = all.first(),
+ item, i
+ ;
+
+ self.clearSelected();
+
+ for(i = 0; i < all.length; i++)
+ {
+ item = $(all[i]);
+
+ if(self.itemManager().compareItems(item.data(CSS_SUGGESTION), suggestion))
+ {
+ target = item.addClass(CSS_SELECTED);
+ break;
+ }
+ }
+
+ target.addClass(CSS_SELECTED);
+ self.scrollSuggestionIntoView(target);
+ };
+
+ /**
+ * Returns the first suggestion HTML element from the dropdown that is highlighted as selected.
+ *
+ * @signature TextExtAutocomplete.selectedSuggestionElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.selectedSuggestionElement
+ */
+ p.selectedSuggestionElement = function()
+ {
+ return this.suggestionElements().filter(CSS_DOT_SELECTED).first();
+ };
+
+ /**
+ * Returns `true` if dropdown is currently visible, `false` otherwise.
+ *
+ * @signature TextExtAutocomplete.isDropdownVisible()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.isDropdownVisible
+ */
+ p.isDropdownVisible = function()
+ {
+ return this.containerElement().is(':visible') === true;
+ };
+
+ /**
+ * Reacts to the `getFormData` event triggered by the core. Returns data with the
+ * weight of 100 to be *less than the Tags plugin* data weight. The weights system is
+ * covered in greater detail in the [`getFormData`][1] event documentation.
+ *
+ * [1]: /manual/textext.html#getformdata
+ *
+ * @signature TextExtAutocomplete.onGetFormData(e, data, keyCode)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Data object to be populated.
+ * @param keyCode {Number} Key code that triggered the original update request.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtAutocomplete.onGetFormData
+ */
+ p.onGetFormData = function(e, data, keyCode)
+ {
+ var self = this,
+ val = self.val(),
+ inputValue = val,
+ formValue = val
+ ;
+ data[100] = self.formDataObject(inputValue, formValue);
+ };
+
+ /**
+ * Returns initialization priority of the Autocomplete plugin which is expected to be
+ * *greater than the Tags plugin* because of the dependencies. The value is 200.
+ *
+ * @signature TextExtAutocomplete.initPriority()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtAutocomplete.initPriority
+ */
+ p.initPriority = function()
+ {
+ return 200;
+ };
+
+ /**
+ * Reacts to the `hideDropdown` event and hides the dropdown if it's already visible.
+ *
+ * @signature TextExtAutocomplete.onHideDropdown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onHideDropdown
+ */
+ p.onHideDropdown = function(e)
+ {
+ this.hideDropdown();
+ };
+
+ /**
+ * Reacts to the 'toggleDropdown` event and shows or hides the dropdown depending if
+ * it's currently hidden or visible.
+ *
+ * @signature TextExtAutocomplete.onToggleDropdown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/12/27
+ * @id TextExtAutocomplete.onToggleDropdown
+ * @version 1.1.0
+ */
+ p.onToggleDropdown = function(e)
+ {
+ var self = this;
+ self.trigger(self.containerElement().is(':visible') ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
+ };
+
+ /**
+ * Reacts to the `showDropdown` event and shows the dropdown if it's not already visible.
+ * It's possible to pass a render callback function which will be called instead of the
+ * default `TextExtAutocomplete.renderSuggestions()`.
+ *
+ * If no suggestion were previously loaded, it will fire `getSuggestions` event and exit.
+ *
+ * Here's how another plugin should trigger this event with the optional render callback:
+ *
+ * this.trigger('showDropdown', function(autocomplete)
+ * {
+ * autocomplete.clearItems();
+ * var node = autocomplete.addDropdownItem('<b>Item</b>');
+ * node.addClass('new-look');
+ * });
+ *
+ * @signature TextExtAutocomplete.onShowDropdown(e, renderCallback)
+ *
+ * @param e {Object} jQuery event.
+ * @param renderCallback {Function} Optional callback function which would be used to
+ * render dropdown items. As a first argument, reference to the current instance of
+ * Autocomplete plugin will be supplied. It's assumed, that if this callback is provided
+ * rendering will be handled completely manually.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onShowDropdown
+ */
+ p.onShowDropdown = function(e, renderCallback)
+ {
+ var self = this,
+ current = self.selectedSuggestionElement().data(CSS_SUGGESTION),
+ suggestions = self._suggestions
+ ;
+
+ if(!suggestions)
+ return self.trigger(EVENT_GET_SUGGESTIONS);
+
+ if($.isFunction(renderCallback))
+ {
+ renderCallback(self);
+ }
+ else
+ {
+ self.renderSuggestions(self._suggestions);
+ self.toggleNextSuggestion();
+ }
+
+ self.showDropdown(self.containerElement());
+ self.setSelectedSuggestion(current);
+ };
+
+ /**
+ * Reacts to the `setSuggestions` event. Expects to recieve the payload as the second argument
+ * in the following structure:
+ *
+ * {
+ * result : [ "item1", "item2" ],
+ * showHideDropdown : false
+ * }
+ *
+ * Notice the optional `showHideDropdown` option. By default, ie without the `showHideDropdown`
+ * value the method will trigger either `showDropdown` or `hideDropdown` depending if there are
+ * suggestions. If set to `false`, no event is triggered.
+ *
+ * @signature TextExtAutocomplete.onSetSuggestions(e, data)
+ *
+ * @param data {Object} Data payload.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.onSetSuggestions
+ */
+ p.onSetSuggestions = function(e, data)
+ {
+ var self = this,
+ suggestions = self._suggestions = data.result
+ ;
+
+ if(data.showHideDropdown !== false)
+ self.trigger(suggestions === null || suggestions.length === 0 ? EVENT_HIDE_DROPDOWN : EVENT_SHOW_DROPDOWN);
+ };
+
+ /**
+ * Prepears for and triggers the `getSuggestions` event with the `{ query : {String} }` as second
+ * argument.
+ *
+ * @signature TextExtAutocomplete.getSuggestions()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.getSuggestions
+ */
+ p.getSuggestions = function()
+ {
+ var self = this,
+ val = self.val()
+ ;
+
+ if(self._previousInputValue == val)
+ return;
+
+ // if user clears input, then we want to select first suggestion
+ // instead of the last one
+ if(val == '')
+ current = null;
+
+ self._previousInputValue = val;
+ self.trigger(EVENT_GET_SUGGESTIONS, { query : val });
+ };
+
+ /**
+ * Removes all HTML suggestion items from the dropdown.
+ *
+ * @signature TextExtAutocomplete.clearItems()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.clearItems
+ */
+ p.clearItems = function()
+ {
+ this.containerElement().find('.text-list').children().remove();
+ };
+
+ /**
+ * Clears all and renders passed suggestions.
+ *
+ * @signature TextExtAutocomplete.renderSuggestions(suggestions)
+ *
+ * @param suggestions {Array} List of suggestions to render.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.renderSuggestions
+ */
+ p.renderSuggestions = function(suggestions)
+ {
+ var self = this;
+
+ self.clearItems();
+
+ $.each(suggestions || [], function(index, item)
+ {
+ self.addSuggestion(item);
+ });
+ };
+
+ /**
+ * Shows the dropdown.
+ *
+ * @signature TextExtAutocomplete.showDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.showDropdown
+ */
+ p.showDropdown = function()
+ {
+ this.containerElement().show();
+ };
+
+ /**
+ * Hides the dropdown.
+ *
+ * @signature TextExtAutocomplete.hideDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.hideDropdown
+ */
+ p.hideDropdown = function()
+ {
+ var self = this,
+ dropdown = self.containerElement()
+ ;
+
+ self._previousInputValue = null;
+ dropdown.hide();
+ };
+
+ /**
+ * Adds single suggestion to the bottom of the dropdown. Uses `ItemManager.itemToString()` to
+ * serialize provided suggestion to string.
+ *
+ * @signature TextExtAutocomplete.addSuggestion(suggestion)
+ *
+ * @param suggestion {Object} Suggestion item. By default expected to be a string.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.addSuggestion
+ */
+ p.addSuggestion = function(suggestion)
+ {
+ var self = this,
+ renderer = self.opts(OPT_RENDER),
+ node = self.addDropdownItem(renderer ? renderer.call(self, suggestion) : self.itemManager().itemToString(suggestion))
+ ;
+
+ node.data(CSS_SUGGESTION, suggestion);
+ };
+
+ /**
+ * Adds and returns HTML node to the bottom of the dropdown.
+ *
+ * @signature TextExtAutocomplete.addDropdownItem(html)
+ *
+ * @param html {String} HTML to be inserted into the item.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.addDropdownItem
+ */
+ p.addDropdownItem = function(html)
+ {
+ var self = this,
+ container = self.containerElement().find('.text-list'),
+ node = $(self.opts(OPT_HTML_SUGGESTION))
+ ;
+
+ node.find('.text-label').html(html);
+ container.append(node);
+ return node;
+ };
+
+ /**
+ * Removes selection highlight from all suggestion elements.
+ *
+ * @signature TextExtAutocomplete.clearSelected()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id TextExtAutocomplete.clearSelected
+ */
+ p.clearSelected = function()
+ {
+ this.suggestionElements().removeClass(CSS_SELECTED);
+ };
+
+ /**
+ * Selects next suggestion relative to the current one. If there's no
+ * currently selected suggestion, it will select the first one. Selected
+ * suggestion will always be scrolled into view.
+ *
+ * @signature TextExtAutocomplete.toggleNextSuggestion()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id TextExtAutocomplete.toggleNextSuggestion
+ */
+ p.toggleNextSuggestion = function()
+ {
+ var self = this,
+ selected = self.selectedSuggestionElement(),
+ next
+ ;
+
+ if(selected.length > 0)
+ {
+ next = selected.next();
+
+ if(next.length > 0)
+ selected.removeClass(CSS_SELECTED);
+ }
+ else
+ {
+ next = self.suggestionElements().first();
+ }
+
+ next.addClass(CSS_SELECTED);
+ self.scrollSuggestionIntoView(next);
+ };
+
+ /**
+ * Selects previous suggestion relative to the current one. Selected
+ * suggestion will always be scrolled into view.
+ *
+ * @signature TextExtAutocomplete.togglePreviousSuggestion()
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id TextExtAutocomplete.togglePreviousSuggestion
+ */
+ p.togglePreviousSuggestion = function()
+ {
+ var self = this,
+ selected = self.selectedSuggestionElement(),
+ prev = selected.prev()
+ ;
+
+ if(prev.length == 0)
+ return;
+
+ self.clearSelected();
+ prev.addClass(CSS_SELECTED);
+ self.scrollSuggestionIntoView(prev);
+ };
+
+ /**
+ * Scrolls specified HTML suggestion element into the view.
+ *
+ * @signature TextExtAutocomplete.scrollSuggestionIntoView(item)
+ *
+ * @param item {HTMLElement} jQuery HTML suggestion element which needs to
+ * scrolled into view.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.scrollSuggestionIntoView
+ */
+ p.scrollSuggestionIntoView = function(item)
+ {
+ var itemHeight = item.outerHeight(),
+ dropdown = this.containerElement(),
+ dropdownHeight = dropdown.innerHeight(),
+ scrollPos = dropdown.scrollTop(),
+ itemTop = (item.position() || {}).top,
+ scrollTo = null,
+ paddingTop = parseInt(dropdown.css('paddingTop'))
+ ;
+
+ if(itemTop == null)
+ return;
+
+ // if scrolling down and item is below the bottom fold
+ if(itemTop + itemHeight > dropdownHeight)
+ scrollTo = itemTop + scrollPos + itemHeight - dropdownHeight + paddingTop;
+
+ // if scrolling up and item is above the top fold
+ if(itemTop < 0)
+ scrollTo = itemTop + scrollPos - paddingTop;
+
+ if(scrollTo != null)
+ dropdown.scrollTop(scrollTo);
+ };
+
+ /**
+ * Uses the value from the text input to finish autocomplete action. Currently selected
+ * suggestion from the dropdown will be used to complete the action. Triggers `hideDropdown`
+ * event.
+ *
+ * @signature TextExtAutocomplete.selectFromDropdown()
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtAutocomplete.selectFromDropdown
+ */
+ p.selectFromDropdown = function()
+ {
+ var self = this,
+ suggestion = self.selectedSuggestionElement().data(CSS_SUGGESTION)
+ ;
+
+ if(suggestion)
+ {
+ self.val(self.itemManager().itemToString(suggestion));
+ self.core().getFormData();
+ }
+
+ self.trigger(EVENT_HIDE_DROPDOWN);
+ };
+
+ /**
+ * Determines if the specified HTML element is within the TextExt core wrap HTML element.
+ *
+ * @signature TextExtAutocomplete.withinWrapElement(element)
+ *
+ * @param element {HTMLElement} element to check if contained by wrap element
+ *
+ * @author adamayres
+ * @version 1.3.0
+ * @date 2012/01/15
+ * @id TextExtAutocomplete.withinWrapElement
+ */
+ p.withinWrapElement = function(element)
+ {
+ return this.core().wrapElement().find(element).size() > 0;
+ }
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.filter.js b/src/main/webapp/textext/textext.plugin.filter.js
new file mode 100644
index 00000000..6f973e6e
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.filter.js
@@ -0,0 +1,242 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * The Filter plugin introduces ability to limit input that the text field
+ * will accept. If the Tags plugin is used, Filter plugin will limit which
+ * tags it's possible to add.
+ *
+ * The list of allowed items can be either specified through the
+ * options, can come from the Suggestions plugin or be loaded by the Ajax
+ * plugin. All these plugins have one thing in common -- they
+ * trigger `setSuggestions` event which the Filter plugin is expecting.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter
+ */
+ function TextExtFilter() {};
+
+ $.fn.textext.TextExtFilter = TextExtFilter;
+ $.fn.textext.addPlugin('filter', TextExtFilter);
+
+ var p = TextExtFilter.prototype,
+
+ /**
+ * Filter plugin options are grouped under `filter` when passed to the
+ * `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'filter',
+ * filter: {
+ * items: [ "item1", "item2" ]
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.options
+ */
+
+ /**
+ * This is a toggle switch to enable or disable the Filter plugin. The value is checked
+ * each time at the top level which allows you to toggle this setting on the fly.
+ *
+ * @name filter.enabled
+ * @default true
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.options.enabled
+ */
+ OPT_ENABLED = 'filter.enabled',
+
+ /**
+ * Arra of items that the Filter plugin will allow the Tag plugin to add to the list of
+ * its resut tags. Each item by default is expected to be a string which default `ItemManager`
+ * can work with. You can change the item type by supplying custom `ItemManager`.
+ *
+ * @name filter.items
+ * @default null
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.options.items
+ */
+ OPT_ITEMS = 'filter.items',
+
+ /**
+ * Filter plugin dispatches and reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.events
+ */
+
+ /**
+ * Filter plugin reacts to the `isTagAllowed` event triggered by the Tags plugin before
+ * adding a new tag to the list. If the new tag is among the `items` specified in options,
+ * then the new tag will be allowed.
+ *
+ * @name isTagAllowed
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.events.isTagAllowed
+ */
+
+ /**
+ * Filter plugin reacts to the `setSuggestions` event triggered by other plugins like
+ * Suggestions and Ajax.
+ *
+ * However, event if this event is handled and items are passed with it and stored, if `items`
+ * option was supplied, it will always take precedense.
+ *
+ * @name setSuggestions
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.events.setSuggestions
+ */
+
+ DEFAULT_OPTS = {
+ filter : {
+ enabled : true,
+ items : null
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtFilter.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.init
+ */
+ p.init = function(core)
+ {
+ var self = this;
+ self.baseInit(core, DEFAULT_OPTS);
+
+ self.on({
+ getFormData : self.onGetFormData,
+ isTagAllowed : self.onIsTagAllowed,
+ setSuggestions : self.onSetSuggestions
+ });
+
+ self._suggestions = null;
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
+ * weight of 200 to be *greater than the Autocomplete plugins* data weights.
+ * The weights system is covered in greater detail in the [`getFormData`][1] event
+ * documentation.
+ *
+ * This method does nothing if Tags tag is also present.
+ *
+ * [1]: /manual/textext.html#getformdata
+ *
+ * @signature TextExtFilter.onGetFormData(e, data, keyCode)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Data object to be populated.
+ * @param keyCode {Number} Key code that triggered the original update request.
+ *
+ * @author agorbatchev
+ * @date 2011/12/28
+ * @id TextExtFilter.onGetFormData
+ * @version 1.1
+ */
+ p.onGetFormData = function(e, data, keyCode)
+ {
+ var self = this,
+ val = self.val(),
+ inputValue = val,
+ formValue = ''
+ ;
+
+ if(!self.core().hasPlugin('tags'))
+ {
+ if(self.isValueAllowed(inputValue))
+ formValue = val;
+
+ data[300] = self.formDataObject(inputValue, formValue);
+ }
+ };
+
+ /**
+ * Checks given value if it's present in `filterItems` or was loaded for the Autocomplete
+ * or by the Suggestions plugins. `value` is compared to each item using `ItemManager.compareItems`
+ * method which is currently attached to the core. Returns `true` if value is known or
+ * Filter plugin is disabled.
+ *
+ * @signature TextExtFilter.isValueAllowed(value)
+ *
+ * @param value {Object} Value to check.
+ *
+ * @author agorbatchev
+ * @date 2011/12/28
+ * @id TextExtFilter.isValueAllowed
+ * @version 1.1
+ */
+ p.isValueAllowed = function(value)
+ {
+ var self = this,
+ list = self.opts('filterItems') || self._suggestions || [],
+ itemManager = self.itemManager(),
+ result = !self.opts(OPT_ENABLED), // if disabled, should just return true
+ i
+ ;
+
+ for(i = 0; i < list.length && !result; i++)
+ if(itemManager.compareItems(value, list[i]))
+ result = true;
+
+ return result;
+ };
+
+ /**
+ * Handles `isTagAllowed` event dispatched by the Tags plugin. If supplied tag is not
+ * in the `items` list, method sets `result` on the `data` argument to `false`.
+ *
+ * @signature TextExtFilter.onIsTagAllowed(e, data)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Payload in the following format : `{ tag : {Object}, result : {Boolean} }`.
+ * @author agorbatchev
+ * @date 2011/08/04
+ * @id TextExtFilter.onIsTagAllowed
+ */
+ p.onIsTagAllowed = function(e, data)
+ {
+ data.result = this.isValueAllowed(data.tag);
+ };
+
+ /**
+ * Reacts to the `setSuggestions` events and stores supplied suggestions for future use.
+ *
+ * @signature TextExtFilter.onSetSuggestions(e, data)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Payload in the following format : `{ result : {Array} } }`.
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFilter.onSetSuggestions
+ */
+ p.onSetSuggestions = function(e, data)
+ {
+ this._suggestions = data.result;
+ };
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.focus.css b/src/main/webapp/textext/textext.plugin.focus.css
new file mode 100644
index 00000000..9579128b
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.focus.css
@@ -0,0 +1,12 @@
+.text-core .text-wrap .text-focus {
+ -webkit-box-shadow: 0px 0px 6px #6d84b4;
+ -moz-box-shadow: 0px 0px 6px #6d84b4;
+ box-shadow: 0px 0px 6px #6d84b4;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: none;
+}
+.text-core .text-wrap .text-focus.text-show-focus {
+ display: block;
+}
diff --git a/src/main/webapp/textext/textext.plugin.focus.js b/src/main/webapp/textext/textext.plugin.focus.js
new file mode 100644
index 00000000..5f7f3575
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.focus.js
@@ -0,0 +1,174 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Focus plugin displays a visual effect whenever user sets focus
+ * into the text area.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus
+ */
+ function TextExtFocus() {};
+
+ $.fn.textext.TextExtFocus = TextExtFocus;
+ $.fn.textext.addPlugin('focus', TextExtFocus);
+
+ var p = TextExtFocus.prototype,
+ /**
+ * Focus plugin only has one option and that is its HTML template. It could be
+ * changed when passed to the `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'focus',
+ * html: {
+ * focus: "<span/>"
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus.options
+ */
+
+ /**
+ * HTML source that is used to generate markup required for the focus effect.
+ *
+ * @name html.focus
+ * @default '<div class="text-focus"/>'
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus.options.html.focus
+ */
+ OPT_HTML_FOCUS = 'html.focus',
+
+ /**
+ * Focus plugin dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtFocus.events
+ */
+
+ /**
+ * Focus plugin reacts to the `focus` event and shows the markup generated from
+ * the `html.focus` option.
+ *
+ * @name focus
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus.events.focus
+ */
+
+ /**
+ * Focus plugin reacts to the `blur` event and hides the effect.
+ *
+ * @name blur
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus.events.blur
+ */
+
+ DEFAULT_OPTS = {
+ html : {
+ focus : '<div class="text-focus"/>'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtFocus.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtFocus.init
+ */
+ p.init = function(core)
+ {
+ var self = this;
+
+ self.baseInit(core, DEFAULT_OPTS);
+ self.core().wrapElement().append(self.opts(OPT_HTML_FOCUS));
+ self.on({
+ blur : self.onBlur,
+ focus : self.onFocus
+ });
+
+ self._timeoutId = 0;
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `blur` event and hides the focus effect with a slight delay which
+ * allows quick refocusing without effect blinking in and out.
+ *
+ * @signature TextExtFocus.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtFocus.onBlur
+ */
+ p.onBlur = function(e)
+ {
+ var self = this;
+
+ clearTimeout(self._timeoutId);
+
+ self._timeoutId = setTimeout(function()
+ {
+ self.getFocus().hide();
+ },
+ 100);
+ };
+
+ /**
+ * Reacts to the `focus` event and shows the focus effect.
+ *
+ * @signature TextExtFocus.onFocus
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtFocus.onFocus
+ */
+ p.onFocus = function(e)
+ {
+ var self = this;
+
+ clearTimeout(self._timeoutId);
+
+ self.getFocus().show();
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * Returns focus effect HTML element.
+ *
+ * @signature TextExtFocus.getFocus()
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtFocus.getFocus
+ */
+ p.getFocus = function()
+ {
+ return this.core().wrapElement().find('.text-focus');
+ };
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.prompt.css b/src/main/webapp/textext/textext.plugin.prompt.css
new file mode 100644
index 00000000..49eab49b
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.prompt.css
@@ -0,0 +1,16 @@
+.text-core .text-wrap .text-prompt {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ margin: 1px 0 0 2px;
+ font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
+ color: #c0c0c0;
+ overflow: hidden;
+ white-space: pre;
+}
+.text-core .text-wrap .text-prompt.text-hide-prompt {
+ display: none;
+}
diff --git a/src/main/webapp/textext/textext.plugin.prompt.js b/src/main/webapp/textext/textext.plugin.prompt.js
new file mode 100644
index 00000000..a40e71d8
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.prompt.js
@@ -0,0 +1,309 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Prompt plugin displays a visual user propmpt in the text input area. If user focuses
+ * on the input, the propt is hidden and only shown again when user focuses on another
+ * element and text input doesn't have a value.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt
+ */
+ function TextExtPrompt() {};
+
+ $.fn.textext.TextExtPrompt = TextExtPrompt;
+ $.fn.textext.addPlugin('prompt', TextExtPrompt);
+
+ var p = TextExtPrompt.prototype,
+
+ CSS_HIDE_PROMPT = 'text-hide-prompt',
+
+ /**
+ * Prompt plugin has options to change the prompt label and its HTML template. The options
+ * could be changed when passed to the `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'prompt',
+ * prompt: 'Your email address'
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.options
+ */
+
+ /**
+ * Prompt message that is displayed to the user whenever there's no value in the input.
+ *
+ * @name prompt
+ * @default 'Awaiting input...'
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.options.prompt
+ */
+ OPT_PROMPT = 'prompt',
+
+ /**
+ * HTML source that is used to generate markup required for the prompt effect.
+ *
+ * @name html.prompt
+ * @default '<div class="text-prompt"/>'
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.options.html.prompt
+ */
+ OPT_HTML_PROMPT = 'html.prompt',
+
+ /**
+ * Prompt plugin dispatches or reacts to the following events.
+ * @id TextExtPrompt.events
+ */
+
+ /**
+ * Prompt plugin reacts to the `focus` event and hides the markup generated from
+ * the `html.prompt` option.
+ *
+ * @name focus
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.events.focus
+ */
+
+ /**
+ * Prompt plugin reacts to the `blur` event and shows the prompt back if user
+ * hasn't entered any value.
+ *
+ * @name blur
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.events.blur
+ */
+
+ DEFAULT_OPTS = {
+ prompt : 'Awaiting input...',
+
+ html : {
+ prompt : '<div class="text-prompt"/>'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtPrompt.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.init
+ */
+ p.init = function(core)
+ {
+ var self = this,
+ placeholderKey = 'placeholder',
+ container,
+ prompt
+ ;
+
+ self.baseInit(core, DEFAULT_OPTS);
+
+ container = $(self.opts(OPT_HTML_PROMPT));
+ $(self).data('container', container);
+
+ self.core().wrapElement().append(container);
+ self.setPrompt(self.opts(OPT_PROMPT));
+
+ prompt = core.input().attr(placeholderKey);
+
+ if(!prompt)
+ prompt = self.opts(OPT_PROMPT);
+
+ // clear placeholder attribute if set
+ core.input().attr(placeholderKey, '');
+
+ if(prompt)
+ self.setPrompt(prompt);
+
+ if($.trim(self.val()).length > 0)
+ self.hidePrompt();
+
+ self.on({
+ blur : self.onBlur,
+ focus : self.onFocus,
+ postInvalidate : self.onPostInvalidate,
+ postInit : self.onPostInit
+ });
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `postInit` and configures the plugin for initial display.
+ *
+ * @signature TextExtPrompt.onPostInit(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id TextExtPrompt.onPostInit
+ */
+ p.onPostInit = function(e)
+ {
+ this.invalidateBounds();
+ };
+
+ /**
+ * Reacts to the `postInvalidate` and insures that prompt display remains correct.
+ *
+ * @signature TextExtPrompt.onPostInvalidate(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id TextExtPrompt.onPostInvalidate
+ */
+ p.onPostInvalidate = function(e)
+ {
+ this.invalidateBounds();
+ };
+
+ /**
+ * Repositions the prompt to make sure it's always at the same place as in the text input carret.
+ *
+ * @signature TextExtPrompt.invalidateBounds()
+ *
+ * @author agorbatchev
+ * @date 2011/08/24
+ * @id TextExtPrompt.invalidateBounds
+ */
+ p.invalidateBounds = function()
+ {
+ var self = this,
+ input = self.input()
+ ;
+
+ self.containerElement().css({
+ paddingLeft : input.css('paddingLeft'),
+ paddingTop : input.css('paddingTop')
+ });
+ };
+
+ /**
+ * Reacts to the `blur` event and shows the prompt effect with a slight delay which
+ * allows quick refocusing without effect blinking in and out.
+ *
+ * The prompt is restored if the text box has no value.
+ *
+ * @signature TextExtPrompt.onBlur(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtPrompt.onBlur
+ */
+ p.onBlur = function(e)
+ {
+ var self = this;
+
+ self.startTimer('prompt', 0.1, function()
+ {
+ self.showPrompt();
+ });
+ };
+
+ /**
+ * Shows prompt HTML element.
+ *
+ * @signature TextExtPrompt.showPrompt()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtPrompt.showPrompt
+ */
+ p.showPrompt = function()
+ {
+ var self = this,
+ input = self.input()
+ ;
+
+ if($.trim(self.val()).length === 0 && !input.is(':focus'))
+ self.containerElement().removeClass(CSS_HIDE_PROMPT);
+ };
+
+ /**
+ * Hides prompt HTML element.
+ *
+ * @signature TextExtPrompt.hidePrompt()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtPrompt.hidePrompt
+ */
+ p.hidePrompt = function()
+ {
+ this.stopTimer('prompt');
+ this.containerElement().addClass(CSS_HIDE_PROMPT);
+ };
+
+ /**
+ * Reacts to the `focus` event and hides the prompt effect.
+ *
+ * @signature TextExtPrompt.onFocus
+ *
+ * @param e {Object} jQuery event.
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtPrompt.onFocus
+ */
+ p.onFocus = function(e)
+ {
+ this.hidePrompt();
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * Sets the prompt display to the specified string.
+ *
+ * @signature TextExtPrompt.setPrompt(str)
+ *
+ * @oaram str {String} String that will be displayed in the prompt.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtPrompt.setPrompt
+ */
+ p.setPrompt = function(str)
+ {
+ this.containerElement().text(str);
+ };
+
+ /**
+ * Returns prompt effect HTML element.
+ *
+ * @signature TextExtPrompt.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtPrompt.containerElement
+ */
+ p.containerElement = function()
+ {
+ return $(this).data('container');
+ };
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.suggestions.js b/src/main/webapp/textext/textext.plugin.suggestions.js
new file mode 100644
index 00000000..1e04613b
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.suggestions.js
@@ -0,0 +1,175 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Suggestions plugin allows to easily specify the list of suggestion items that the
+ * Autocomplete plugin would present to the user.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtSuggestions
+ */
+ function TextExtSuggestions() {};
+
+ $.fn.textext.TextExtSuggestions = TextExtSuggestions;
+ $.fn.textext.addPlugin('suggestions', TextExtSuggestions);
+
+ var p = TextExtSuggestions.prototype,
+ /**
+ * Suggestions plugin only has one option and that is to set suggestion items. It could be
+ * changed when passed to the `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'suggestions',
+ * suggestions: [ "item1", "item2" ]
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtSuggestions.options
+ */
+
+ /**
+ * List of items that Autocomplete plugin would display in the dropdown.
+ *
+ * @name suggestions
+ * @default null
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtSuggestions.options.suggestions
+ */
+ OPT_SUGGESTIONS = 'suggestions',
+
+ /**
+ * Suggestions plugin dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtSuggestions.events
+ */
+
+ /**
+ * Suggestions plugin reacts to the `getSuggestions` event and returns `suggestions` items
+ * from the options.
+ *
+ * @name getSuggestions
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.events.getSuggestions
+ */
+
+ /**
+ * Suggestions plugin triggers the `setSuggestions` event to pass its own list of `Suggestions`
+ * to the Autocomplete plugin.
+ *
+ * @name setSuggestions
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.events.setSuggestions
+ */
+
+ /**
+ * Suggestions plugin reacts to the `postInit` event to pass its list of `suggestions` to the
+ * Autocomplete right away.
+ *
+ * @name postInit
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.events.postInit
+ */
+
+ DEFAULT_OPTS = {
+ suggestions : null
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtSuggestions.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/18
+ * @id TextExtSuggestions.init
+ */
+ p.init = function(core)
+ {
+ var self = this;
+
+ self.baseInit(core, DEFAULT_OPTS);
+
+ self.on({
+ getSuggestions : self.onGetSuggestions,
+ postInit : self.onPostInit
+ });
+ };
+
+ /**
+ * Triggers `setSuggestions` and passes supplied suggestions to the Autocomplete plugin.
+ *
+ * @signature TextExtSuggestions.setSuggestions(suggestions, showHideDropdown)
+ *
+ * @param suggestions {Array} List of suggestions. With the default `ItemManager` it should
+ * be a list of strings.
+ * @param showHideDropdown {Boolean} If it's undesirable to show the dropdown right after
+ * suggestions are set, `false` should be passed for this argument.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.setSuggestions
+ */
+ p.setSuggestions = function(suggestions, showHideDropdown)
+ {
+ this.trigger('setSuggestions', { result : suggestions, showHideDropdown : showHideDropdown != false });
+ };
+
+ /**
+ * Reacts to the `postInit` event and triggers `setSuggestions` event to set suggestions list
+ * right after initialization.
+ *
+ * @signature TextExtSuggestions.onPostInit(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.onPostInit
+ */
+ p.onPostInit = function(e)
+ {
+ var self = this;
+ self.setSuggestions(self.opts(OPT_SUGGESTIONS), false);
+ };
+
+ /**
+ * Reacts to the `getSuggestions` event and triggers `setSuggestions` event with the list
+ * of `suggestions` specified in the options.
+ *
+ * @signature TextExtSuggestions.onGetSuggestions(e, data)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Payload from the `getSuggestions` event with the user query, eg `{ query: {String} }`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtSuggestions.onGetSuggestions
+ */
+ p.onGetSuggestions = function(e, data)
+ {
+ var self = this,
+ suggestions = self.opts(OPT_SUGGESTIONS)
+ ;
+
+ suggestions.sort();
+ self.setSuggestions(self.itemManager().filter(suggestions, data.query));
+ };
+})(jQuery);
diff --git a/src/main/webapp/textext/textext.plugin.tags.css b/src/main/webapp/textext/textext.plugin.tags.css
new file mode 100644
index 00000000..bdbdcc43
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.tags.css
@@ -0,0 +1,49 @@
+.text-core .text-wrap .text-tags {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ padding: 3px 35px 3px 3px;
+ cursor: text;
+}
+.text-core .text-wrap .text-tags.text-tags-on-top {
+ z-index: 2;
+}
+.text-core .text-wrap .text-tags .text-tag {
+ float: left;
+}
+.text-core .text-wrap .text-tags .text-tag .text-button {
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ position: relative;
+ float: left;
+ border: 1px solid #9daccc;
+ background: #e2e6f0;
+ color: #000;
+ padding: 0px 17px 0px 3px;
+ margin: 0 2px 2px 0;
+ cursor: pointer;
+ height: 16px;
+ font: 11px "lucida grande", tahoma, verdana, arial, sans-serif;
+}
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove {
+ position: absolute;
+ right: 3px;
+ top: 2px;
+ display: block;
+ width: 11px;
+ height: 11px;
+ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAhCAYAAAAPm1F2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB50RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNS4xqx9I6wAAAQ5JREFUOI2dlD0WwiAQhCc8L6HHgAPoASwtSYvX8BrQxtIyveYA8RppLO1jE+LwE8lzms2yH8MCj1QoaBzH+VuUYNYMS213UlvDRamtUbXb5ZyPHuDoxwGgip3ipfvGuGzPz+vZ/coDONdzFuYCO6ramQQG0DJIE1oPBBvM6e9LqaS2FwD7FWwnVoIAsOc2Xn1jDlyd8pfPBRVOBHA8cc/3yCmQqt0jcY4LuTyAF3pOYS6wI48LAm4MUrx5JthgSQJAt5LtNgAUgEMBBIC3AL2xgo58dEPfhE9wygef89FtCeC49UwltR1pQrK2qr9vNr7uRTCBF3pOYS6wI4/zdQ8MUpxPI9hgSQL0Xyio/QBt54DzsHQx6gAAAABJRU5ErkJggg==") 0 0 no-repeat;
+}
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:hover {
+ background-position: 0 -11px;
+}
+.text-core .text-wrap .text-tags .text-tag .text-button a.text-remove:active {
+ background-position: 0 -22px;
+}
diff --git a/src/main/webapp/textext/textext.plugin.tags.js b/src/main/webapp/textext/textext.plugin.tags.js
new file mode 100644
index 00000000..3839badb
--- /dev/null
+++ b/src/main/webapp/textext/textext.plugin.tags.js
@@ -0,0 +1,692 @@
+/**
+ * jQuery TextExt Plugin
+ * http://textextjs.com
+ *
+ * @version 1.3.1
+ * @copyright Copyright (C) 2011 Alex Gorbatchev. All rights reserved.
+ * @license MIT License
+ */
+(function($)
+{
+ /**
+ * Tags plugin brings in the traditional tag functionality where user can assemble and
+ * edit list of tags. Tags plugin works especially well together with Autocomplete, Filter,
+ * Suggestions and Ajax plugins to provide full spectrum of features. It can also work on
+ * its own and just do one thing -- tags.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags
+ */
+ function TextExtTags() {};
+
+ $.fn.textext.TextExtTags = TextExtTags;
+ $.fn.textext.addPlugin('tags', TextExtTags);
+
+ var p = TextExtTags.prototype,
+
+ CSS_DOT = '.',
+ CSS_TAGS_ON_TOP = 'text-tags-on-top',
+ CSS_DOT_TAGS_ON_TOP = CSS_DOT + CSS_TAGS_ON_TOP,
+ CSS_TAG = 'text-tag',
+ CSS_DOT_TAG = CSS_DOT + CSS_TAG,
+ CSS_TAGS = 'text-tags',
+ CSS_DOT_TAGS = CSS_DOT + CSS_TAGS,
+ CSS_LABEL = 'text-label',
+ CSS_DOT_LABEL = CSS_DOT + CSS_LABEL,
+ CSS_REMOVE = 'text-remove',
+ CSS_DOT_REMOVE = CSS_DOT + CSS_REMOVE,
+
+ /**
+ * Tags plugin options are grouped under `tags` when passed to the
+ * `$().textext()` function. For example:
+ *
+ * $('textarea').textext({
+ * plugins: 'tags',
+ * tags: {
+ * items: [ "tag1", "tag2" ]
+ * }
+ * })
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.options
+ */
+
+ /**
+ * This is a toggle switch to enable or disable the Tags plugin. The value is checked
+ * each time at the top level which allows you to toggle this setting on the fly.
+ *
+ * @name tags.enabled
+ * @default true
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.options.tags.enabled
+ */
+ OPT_ENABLED = 'tags.enabled',
+
+ /**
+ * Allows to specify tags which will be added to the input by default upon initialization.
+ * Each item in the array must be of the type that current `ItemManager` can understand.
+ * Default type is `String`.
+ *
+ * @name tags.items
+ * @default null
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.options.tags.items
+ */
+ OPT_ITEMS = 'tags.items',
+
+ /**
+ * HTML source that is used to generate a single tag.
+ *
+ * @name html.tag
+ * @default '<div class="text-tags"/>'
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.options.html.tag
+ */
+ OPT_HTML_TAG = 'html.tag',
+
+ /**
+ * HTML source that is used to generate container for the tags.
+ *
+ * @name html.tags
+ * @default '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.options.html.tags
+ */
+ OPT_HTML_TAGS = 'html.tags',
+
+ /**
+ * Tags plugin dispatches or reacts to the following events.
+ *
+ * @author agorbatchev
+ * @date 2011/08/17
+ * @id TextExtTags.events
+ */
+
+ /**
+ * Tags plugin triggers the `isTagAllowed` event before adding each tag to the tag list. Other plugins have
+ * an opportunity to interrupt this by setting `result` of the second argument to `false`. For example:
+ *
+ * $('textarea').textext({...}).bind('isTagAllowed', function(e, data)
+ * {
+ * if(data.tag === 'foo')
+ * data.result = false;
+ * })
+ *
+ * The second argument `data` has the following format: `{ tag : {Object}, result : {Boolean} }`. `tag`
+ * property is in the format that the current `ItemManager` can understand.
+ *
+ * @name isTagAllowed
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.events.isTagAllowed
+ */
+ EVENT_IS_TAG_ALLOWED = 'isTagAllowed',
+
+ /**
+ * Tags plugin triggers the `tagClick` event when user clicks on one of the tags. This allows to process
+ * the click and potentially change the value of the tag (for example in case of user feedback).
+ *
+ * $('textarea').textext({...}).bind('tagClick', function(e, tag, value, callback)
+ * {
+ * var newValue = window.prompt('New value', value);
+
+ * if(newValue)
+ * callback(newValue, true);
+ * })
+ *
+ * Callback argument has the following signature:
+ *
+ * function(newValue, refocus)
+ * {
+ * ...
+ * }
+ *
+ * Please check out [example](/manual/examples/tags-changing.html).
+ *
+ * @name tagClick
+ * @version 1.3.0
+ * @author s.stok
+ * @date 2011/01/23
+ * @id TextExtTags.events.tagClick
+ */
+ EVENT_TAG_CLICK = 'tagClick',
+
+ DEFAULT_OPTS = {
+ tags : {
+ enabled : true,
+ items : null
+ },
+
+ html : {
+ tags : '<div class="text-tags"/>',
+ tag : '<div class="text-tag"><div class="text-button"><span class="text-label"/><a class="text-remove"/></div></div>'
+ }
+ }
+ ;
+
+ /**
+ * Initialization method called by the core during plugin instantiation.
+ *
+ * @signature TextExtTags.init(core)
+ *
+ * @param core {TextExt} Instance of the TextExt core class.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.init
+ */
+ p.init = function(core)
+ {
+ this.baseInit(core, DEFAULT_OPTS);
+ var self = this,
+ input = self.input(),
+ container
+ ;
+
+ if(self.opts(OPT_ENABLED))
+ {
+ container = $(self.opts(OPT_HTML_TAGS));
+ input.after(container);
+
+ $(self).data('container', container);
+
+ self.on({
+ enterKeyPress : self.onEnterKeyPress,
+ backspaceKeyDown : self.onBackspaceKeyDown,
+ preInvalidate : self.onPreInvalidate,
+ postInit : self.onPostInit,
+ getFormData : self.onGetFormData
+ });
+
+ self.on(container, {
+ click : self.onClick,
+ mousemove : self.onContainerMouseMove
+ });
+
+ self.on(input, {
+ mousemove : self.onInputMouseMove
+ });
+ }
+
+ self._originalPadding = {
+ left : parseInt(input.css('paddingLeft') || 0),
+ top : parseInt(input.css('paddingTop') || 0)
+ };
+
+ self._paddingBox = {
+ left : 0,
+ top : 0
+ };
+
+ self.updateFormCache();
+ };
+
+ /**
+ * Returns HTML element in which all tag HTML elements are residing.
+ *
+ * @signature TextExtTags.containerElement()
+ *
+ * @author agorbatchev
+ * @date 2011/08/15
+ * @id TextExtTags.containerElement
+ */
+ p.containerElement = function()
+ {
+ return $(this).data('container');
+ };
+
+ //--------------------------------------------------------------------------------
+ // Event handlers
+
+ /**
+ * Reacts to the `postInit` event triggered by the core and sets default tags
+ * if any were specified.
+ *
+ * @signature TextExtTags.onPostInit(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExtTags.onPostInit
+ */
+ p.onPostInit = function(e)
+ {
+ var self = this;
+ self.addTags(self.opts(OPT_ITEMS));
+ };
+
+ /**
+ * Reacts to the [`getFormData`][1] event triggered by the core. Returns data with the
+ * weight of 200 to be *greater than the Autocomplete plugin* data weight. The weights
+ * system is covered in greater detail in the [`getFormData`][1] event documentation.
+ *
+ * [1]: /manual/textext.html#getformdata
+ *
+ * @signature TextExtTags.onGetFormData(e, data, keyCode)
+ *
+ * @param e {Object} jQuery event.
+ * @param data {Object} Data object to be populated.
+ * @param keyCode {Number} Key code that triggered the original update request.
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtTags.onGetFormData
+ */
+ p.onGetFormData = function(e, data, keyCode)
+ {
+ var self = this,
+ inputValue = keyCode === 13 ? '' : self.val(),
+ formValue = self._formData
+ ;
+
+ data[200] = self.formDataObject(inputValue, formValue);
+ };
+
+ /**
+ * Returns initialization priority of the Tags plugin which is expected to be
+ * *less than the Autocomplete plugin* because of the dependencies. The value is
+ * 100.
+ *
+ * @signature TextExtTags.initPriority()
+ *
+ * @author agorbatchev
+ * @date 2011/08/22
+ * @id TextExtTags.initPriority
+ */
+ p.initPriority = function()
+ {
+ return 100;
+ };
+
+ /**
+ * Reacts to user moving mouse over the text area when cursor is over the text
+ * and not over the tags. Whenever mouse cursor is over the area covered by
+ * tags, the tags container is flipped to be on top of the text area which
+ * makes all tags functional with the mouse.
+ *
+ * @signature TextExtTags.onInputMouseMove(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtTags.onInputMouseMove
+ */
+ p.onInputMouseMove = function(e)
+ {
+ this.toggleZIndex(e);
+ };
+
+ /**
+ * Reacts to user moving mouse over the tags. Whenever the cursor moves out
+ * of the tags and back into where the text input is happening visually,
+ * the tags container is sent back under the text area which allows user
+ * to interact with the text using mouse cursor as expected.
+ *
+ * @signature TextExtTags.onContainerMouseMove(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtTags.onContainerMouseMove
+ */
+ p.onContainerMouseMove = function(e)
+ {
+ this.toggleZIndex(e);
+ };
+
+ /**
+ * Reacts to the `backspaceKeyDown` event. When backspace key is pressed in an empty text field,
+ * deletes last tag from the list.
+ *
+ * @signature TextExtTags.onBackspaceKeyDown(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/02
+ * @id TextExtTags.onBackspaceKeyDown
+ */
+ p.onBackspaceKeyDown = function(e)
+ {
+ var self = this,
+ lastTag = self.tagElements().last()
+ ;
+
+ if(self.val().length == 0)
+ self.removeTag(lastTag);
+ };
+
+ /**
+ * Reacts to the `preInvalidate` event and updates the input box to look like the tags are
+ * positioned inside it.
+ *
+ * @signature TextExtTags.onPreInvalidate(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.onPreInvalidate
+ */
+ p.onPreInvalidate = function(e)
+ {
+ var self = this,
+ lastTag = self.tagElements().last(),
+ pos = lastTag.position()
+ ;
+
+ if(lastTag.length > 0)
+ pos.left += lastTag.innerWidth();
+ else
+ pos = self._originalPadding;
+
+ self._paddingBox = pos;
+
+ self.input().css({
+ paddingLeft : pos.left,
+ paddingTop : pos.top
+ });
+ };
+
+ /**
+ * Reacts to the mouse `click` event.
+ *
+ * @signature TextExtTags.onClick(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.onClick
+ */
+ p.onClick = function(e)
+ {
+ var self = this,
+ core = self.core(),
+ source = $(e.target),
+ focus = 0,
+ tag
+ ;
+
+ if(source.is(CSS_DOT_TAGS))
+ {
+ focus = 1;
+ }
+ else if(source.is(CSS_DOT_REMOVE))
+ {
+ self.removeTag(source.parents(CSS_DOT_TAG + ':first'));
+ focus = 1;
+ }
+ else if(source.is(CSS_DOT_LABEL))
+ {
+ tag = source.parents(CSS_DOT_TAG + ':first');
+ self.trigger(EVENT_TAG_CLICK, tag, tag.data(CSS_TAG), tagClickCallback);
+ }
+
+ function tagClickCallback(newValue, refocus)
+ {
+ tag.data(CSS_TAG, newValue);
+ tag.find(CSS_DOT_LABEL).text(self.itemManager().itemToString(newValue));
+
+ self.updateFormCache();
+ core.getFormData();
+ core.invalidateBounds();
+
+ if(refocus)
+ core.focusInput();
+ }
+
+ if(focus)
+ core.focusInput();
+ };
+
+ /**
+ * Reacts to the `enterKeyPress` event and adds whatever is currently in the text input
+ * as a new tag. Triggers `isTagAllowed` to check if the tag could be added first.
+ *
+ * @signature TextExtTags.onEnterKeyPress(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.onEnterKeyPress
+ */
+ p.onEnterKeyPress = function(e)
+ {
+ var self = this,
+ val = self.val(),
+ tag = self.itemManager().stringToItem(val)
+ ;
+
+ if(self.isTagAllowed(tag))
+ {
+ self.addTags([ tag ]);
+ // refocus the textarea just in case it lost the focus
+ self.core().focusInput();
+ }
+ };
+
+ //--------------------------------------------------------------------------------
+ // Core functionality
+
+ /**
+ * Creates a cache object with all the tags currently added which will be returned
+ * in the `onGetFormData` handler.
+ *
+ * @signature TextExtTags.updateFormCache()
+ *
+ * @author agorbatchev
+ * @date 2011/08/09
+ * @id TextExtTags.updateFormCache
+ */
+ p.updateFormCache = function()
+ {
+ var self = this,
+ result = []
+ ;
+
+ self.tagElements().each(function()
+ {
+ result.push($(this).data(CSS_TAG));
+ });
+
+ // cache the results to be used in the onGetFormData
+ self._formData = result;
+ };
+
+ /**
+ * Toggles tag container to be on top of the text area or under based on where
+ * the mouse cursor is located. When cursor is above the text input and out of
+ * any of the tags, the tags container is sent under the text area. If cursor
+ * is over any of the tags, the tag container is brought to be over the text
+ * area.
+ *
+ * @signature TextExtTags.toggleZIndex(e)
+ *
+ * @param e {Object} jQuery event.
+ *
+ * @author agorbatchev
+ * @date 2011/08/08
+ * @id TextExtTags.toggleZIndex
+ */
+ p.toggleZIndex = function(e)
+ {
+ var self = this,
+ offset = self.input().offset(),
+ mouseX = e.clientX - offset.left,
+ mouseY = e.clientY - offset.top,
+ box = self._paddingBox,
+ container = self.containerElement(),
+ isOnTop = container.is(CSS_DOT_TAGS_ON_TOP),
+ isMouseOverText = mouseX > box.left && mouseY > box.top
+ ;
+
+ if(!isOnTop && !isMouseOverText || isOnTop && isMouseOverText)
+ container[(!isOnTop ? 'add' : 'remove') + 'Class'](CSS_TAGS_ON_TOP);
+ };
+
+ /**
+ * Returns all tag HTML elements.
+ *
+ * @signature TextExtTags.tagElements()
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.tagElements
+ */
+ p.tagElements = function()
+ {
+ return this.containerElement().find(CSS_DOT_TAG);
+ };
+
+ /**
+ * Wrapper around the `isTagAllowed` event which triggers it and returns `true`
+ * if `result` property of the second argument remains `true`.
+ *
+ * @signature TextExtTags.isTagAllowed(tag)
+ *
+ * @param tag {Object} Tag object that the current `ItemManager` can understand.
+ * Default is `String`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.isTagAllowed
+ */
+ p.isTagAllowed = function(tag)
+ {
+ var opts = { tag : tag, result : true };
+ this.trigger(EVENT_IS_TAG_ALLOWED, opts);
+ return opts.result === true;
+ };
+
+ /**
+ * Adds specified tags to the tag list. Triggers `isTagAllowed` event for each tag
+ * to insure that it could be added. Calls `TextExt.getFormData()` to refresh the data.
+ *
+ * @signature TextExtTags.addTags(tags)
+ *
+ * @param tags {Array} List of tags that current `ItemManager` can understand. Default
+ * is `String`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.addTags
+ */
+ p.addTags = function(tags)
+ {
+ if(!tags || tags.length == 0)
+ return;
+
+ var self = this,
+ core = self.core(),
+ container = self.containerElement(),
+ i, tag
+ ;
+
+ for(i = 0; i < tags.length; i++)
+ {
+ tag = tags[i];
+
+ if(tag && self.isTagAllowed(tag))
+ container.append(self.renderTag(tag));
+ }
+
+ self.updateFormCache();
+ core.getFormData();
+ core.invalidateBounds();
+ };
+
+ /**
+ * Returns HTML element for the specified tag.
+ *
+ * @signature TextExtTags.getTagElement(tag)
+ *
+ * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+ * Default is `String`.
+
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.getTagElement
+ */
+ p.getTagElement = function(tag)
+ {
+ var self = this,
+ list = self.tagElements(),
+ i, item
+ ;
+
+ for(i = 0; i < list.length; i++) {
+ item = $(list[i]);
+ if (self.itemManager().compareItems(item.data(CSS_TAG), tag))
+ return item;
+ }
+ };
+
+ /**
+ * Removes specified tag from the list. Calls `TextExt.getFormData()` to refresh the data.
+ *
+ * @signature TextExtTags.removeTag(tag)
+ *
+ * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+ * Default is `String`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.removeTag
+ */
+ p.removeTag = function(tag)
+ {
+ var self = this,
+ core = self.core(),
+ element
+ ;
+
+ if(tag instanceof $)
+ {
+ element = tag;
+ tag = tag.data(CSS_TAG);
+ }
+ else
+ {
+ element = self.getTagElement(tag);
+ }
+
+ element.remove();
+ self.updateFormCache();
+ core.getFormData();
+ core.invalidateBounds();
+ };
+
+ /**
+ * Creates and returns new HTML element from the source code specified in the `html.tag` option.
+ *
+ * @signature TextExtTags.renderTag(tag)
+ *
+ * @param tag {Object} Tag object in the format that current `ItemManager` can understand.
+ * Default is `String`.
+ *
+ * @author agorbatchev
+ * @date 2011/08/19
+ * @id TextExtTags.renderTag
+ */
+ p.renderTag = function(tag)
+ {
+ var self = this,
+ node = $(self.opts(OPT_HTML_TAG))
+ ;
+
+ node.find('.text-label').text(self.itemManager().itemToString(tag));
+ node.data(CSS_TAG, tag);
+ return node;
+ };
+})(jQuery);