user mentions via content editable div was flawed, this fix superposes a textarea and a div to make users look highlighted

git-svn-id: https://svn.research-infrastructures.eu/d4science/gcube/trunk/portlets/user/share-updates@76648 82a268e6-3cf1-43bd-a215-b396298e98cf
This commit is contained in:
Massimiliano Assante 2013-06-02 14:26:58 +00:00
parent 493934bcfd
commit e7b89c36bd
17 changed files with 426 additions and 361 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<classpath> <classpath>
<classpathentry kind="src" output="target/share-updates-0.3.0-SNAPSHOT/WEB-INF/classes" path="src/main/java"> <classpathentry kind="src" output="target/share-updates-0.4.0-SNAPSHOT/WEB-INF/classes" path="src/main/java">
<attributes> <attributes>
<attribute name="optional" value="true"/> <attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
@ -31,5 +31,5 @@
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/> <attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="output" path="target/share-updates-0.3.0-SNAPSHOT/WEB-INF/classes"/> <classpathentry kind="output" path="target/share-updates-0.4.0-SNAPSHOT/WEB-INF/classes"/>
</classpath> </classpath>

View File

@ -1,6 +1,6 @@
#Tue Apr 16 18:09:11 CEST 2013 #Sun Jun 02 00:32:02 CEST 2013
eclipse.preferences.version=1 eclipse.preferences.version=1
jarsExcludedFromWebInfLib= jarsExcludedFromWebInfLib=
lastWarOutDir=/Users/massi/Documents/workspace/share-updates/target/share-updates-0.3.0-SNAPSHOT lastWarOutDir=/Users/massi/Documents/workspace/share-updates/target/share-updates-0.4.0-SNAPSHOT
warSrcDir=src/main/webapp warSrcDir=src/main/webapp
warSrcDirIsOutput=false warSrcDirIsOutput=false

View File

@ -5,9 +5,6 @@
<wb-resource deploy-path="/" source-path="/src/main/webapp" tag="defaultRootSource"/> <wb-resource deploy-path="/" source-path="/src/main/webapp" tag="defaultRootSource"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
<wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-sources/gwt"/> <wb-resource deploy-path="/WEB-INF/classes" source-path="/target/generated-sources/gwt"/>
<dependent-module archiveName="pickuser-widget-0.1.0-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/pickuser-widget/pickuser-widget">
<dependency-type>uses</dependency-type>
</dependent-module>
<property name="java-output-path" value="/${module}/target/www/WEB-INF/classes"/> <property name="java-output-path" value="/${module}/target/www/WEB-INF/classes"/>
<property name="context-root" value="share-updates"/> <property name="context-root" value="share-updates"/>
</wb-module> </wb-module>

View File

@ -13,7 +13,7 @@
<groupId>org.gcube.portlets.user</groupId> <groupId>org.gcube.portlets.user</groupId>
<artifactId>share-updates</artifactId> <artifactId>share-updates</artifactId>
<packaging>war</packaging> <packaging>war</packaging>
<version>0.3.0-SNAPSHOT</version> <version>0.4.0-SNAPSHOT</version>
<name>gCube Share Updates Portlet</name> <name>gCube Share Updates Portlet</name>
<description> <description>
@ -137,11 +137,6 @@
<artifactId>jtidy</artifactId> <artifactId>jtidy</artifactId>
<version>r938</version> <version>r938</version>
</dependency> </dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.6.1</version>
</dependency>
<dependency> <dependency>
<groupId>net.eliasbalasis</groupId> <groupId>net.eliasbalasis</groupId>
<artifactId>tibcopagebus4gwt</artifactId> <artifactId>tibcopagebus4gwt</artifactId>

View File

@ -17,7 +17,7 @@ import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
*/ */
@RemoteServiceRelativePath("shareupdateServlet") @RemoteServiceRelativePath("shareupdateServlet")
public interface ShareUpdateService extends RemoteService { public interface ShareUpdateService extends RemoteService {
ClientFeed share(String feedText, FeedType type, PrivacyLevel pLevel, String vreName, String linkTitle, String linkDesc, String url, String urlThumbnail, String host); ClientFeed share(String feedText, FeedType type, PrivacyLevel pLevel, String vreName, String linkTitle, String linkDesc, String url, String urlThumbnail, String host, ArrayList<String> mentionedUsers);
UserSettings getUserSettings(); UserSettings getUserSettings();

View File

@ -17,7 +17,8 @@ import com.google.gwt.user.client.rpc.AsyncCallback;
public interface ShareUpdateServiceAsync { public interface ShareUpdateServiceAsync {
void share(String feedText, FeedType type, PrivacyLevel pLevel, void share(String feedText, FeedType type, PrivacyLevel pLevel,
String vreName, String linkTitle, String linkDesc, String url, String vreName, String linkTitle, String linkDesc, String url,
String urlThumbnail, String host, AsyncCallback<ClientFeed> callback); String urlThumbnail, String host, ArrayList<String> mentionedUsers,
AsyncCallback<ClientFeed> callback);
void checkLink(String linkToCheck, AsyncCallback<LinkPreview> callback); void checkLink(String linkToCheck, AsyncCallback<LinkPreview> callback);

View File

@ -1,51 +0,0 @@
package org.gcube.portlets.user.shareupdates.client.elements;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasText;
/**
*
* @author Massimiliano Assante, ISTI-CNR
*
*/
public class ContentEditDiv extends HTML implements HasText {
public ContentEditDiv() {
super(DOM.createElement("div"));
//important to make it act like a textarea
DOM.setElementAttribute(getElement(), "contentEditable", "true");
DOM.setElementAttribute(getElement(), "id", "mycontentEditableElement");
}
public ContentEditDiv(String text) {
this();
setText(text);
}
public void setEnabled(boolean enabled) {
DOM.setElementPropertyBoolean(getElement(), "disabled", !enabled);
}
public static native void setEndOfContenteditable() /*-{
var contentEditableElement = $doc.getElementById("mycontentEditableElement");
var range,selection;
if($doc.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = $doc.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = $wnd.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if($doc.selection)//IE 8 and lower
{
range = $doc.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}-*/;
}

View File

@ -1,26 +0,0 @@
package org.gcube.portlets.user.shareupdates.client.elements;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasText;
/**
*
* @author Massimiliano Assante, ISTI-CNR
*
*/
public class Span extends HTML implements HasText {
public Span() {
super(DOM.createElement("span"));
}
public Span(String text) {
this();
setText(text);
}
public void setCSSClassName(String className) {
getElement().addClassName(className);
}
}

View File

@ -1,24 +0,0 @@
package org.gcube.portlets.user.shareupdates.client.elements;
import com.google.gwt.user.client.ui.TextBox;
/**
*
* @author Massimiliano Assante, ISTI-CNR
*
*/
public class TagBox extends TextBox {
public TagBox() {
}
public TagBox(String text) {
super();
this.setStylePrimaryName("user-token");
getElement().setAttribute("value", text);
//random heuristic, the "i" and "l" char occupies very few px :)
int iCounter = 14 - 2*(text.split("i").length + (text.split("l").length));
setWidth((text.length()*6+iCounter)+"px");
setReadOnly(true);
}
}

View File

@ -1,5 +1,7 @@
package org.gcube.portlets.user.shareupdates.client.form; package org.gcube.portlets.user.shareupdates.client.form;
import java.util.ArrayList;
import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapter; import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapter;
import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapterException; import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapterException;
@ -43,7 +45,7 @@ public class ShareUpdateForm extends Composite {
.create(ShareUpdateService.class); .create(ShareUpdateService.class);
final PageBusAdapter pageBusAdapter = new PageBusAdapter(); final PageBusAdapter pageBusAdapter = new PageBusAdapter();
protected final static String SHARE_UPDATE_TEXT = "Share an update or paste a link, use “@” and then start typing to tag people"; protected final static String SHARE_UPDATE_TEXT = "Share an update or paste a link, use “@” to mention someone";
protected final static String ERROR_UPDATE_TEXT = "Looks like empty to me!"; protected final static String ERROR_UPDATE_TEXT = "Looks like empty to me!";
private final static String LISTBOX_LEVEL = " - "; private final static String LISTBOX_LEVEL = " - ";
@ -65,6 +67,18 @@ public class ShareUpdateForm extends Composite {
} }
@UiField @UiField
HTMLPanel mainPanel; HTMLPanel mainPanel;
@UiField
LinkPlaceholder preview;
@UiField
Button submitButton;
@UiField
Image avatarImage;
@UiField SuperPosedTextArea shareTextArea;
@UiField ListBox privacyLevel = new ListBox(false);
UserInfo myUserInfo; UserInfo myUserInfo;
@ -110,18 +124,6 @@ public class ShareUpdateForm extends Composite {
} }
}); });
} }
@UiField
LinkPlaceholder preview;
@UiField
Button submitButton;
@UiField
Image avatarImage;
@UiField SmartTextArea shareTextArea;
@UiField ListBox privacyLevel = new ListBox(false);
@UiHandler("shareTextArea") @UiHandler("shareTextArea")
void onShareUpdateClick(ClickEvent e) { void onShareUpdateClick(ClickEvent e) {
@ -147,8 +149,8 @@ public class ShareUpdateForm extends Composite {
return; return;
} }
//then you can post, but pass html //then you can post, but pass html
String toPost = shareTextArea.getHTML().trim(); String toPost = shareTextArea.getText();
postTweet(toPost); postTweet(toPost, shareTextArea.getMentionedUsers());
} }
}); });
} }
@ -156,15 +158,13 @@ public class ShareUpdateForm extends Composite {
* *
* @param textToPost * @param textToPost
*/ */
private void postTweet(String textToPost) { private void postTweet(String textToPost, ArrayList<String> mentionedUsers) {
//String toShare = escapeHtml(textToPost); String toShare = escapeHtml(textToPost);
String toShare = textToPost; if (! checkTextLength(toShare)) {
if (! checkTextLength(new HTML(toShare).getText())) { //need to convert it to text
Window.alert("We found a single word containing more than 50 chars and it's not a link, is it meaningful?"); Window.alert("We found a single word containing more than 50 chars and it's not a link, is it meaningful?");
return; return;
} }
submitButton.setEnabled(false); submitButton.setEnabled(false);
shareTextArea.setEnabled(false); shareTextArea.setEnabled(false);
@ -186,11 +186,12 @@ public class ShareUpdateForm extends Composite {
linkUrlThumbnail = myLinkPreviewer.getUrlThumbnail(); linkUrlThumbnail = myLinkPreviewer.getUrlThumbnail();
linkHost = myLinkPreviewer.getHost(); linkHost = myLinkPreviewer.getHost();
} }
shareupdateService.share(toShare, FeedType.TWEET, getPrivacyLevel(), vreId, linkTitle, linkDescription, linkUrl, linkUrlThumbnail, linkHost, new AsyncCallback<ClientFeed>() { shareupdateService.share(toShare, FeedType.TWEET, getPrivacyLevel(), vreId, linkTitle, linkDescription, linkUrl, linkUrlThumbnail, linkHost, mentionedUsers, new AsyncCallback<ClientFeed>() {
public void onFailure(Throwable caught) { public void onFailure(Throwable caught) {
submitButton.setEnabled(true); submitButton.setEnabled(true);
shareTextArea.setEnabled(true); shareTextArea.setEnabled(true);
shareTextArea.setText(SHARE_UPDATE_TEXT); shareTextArea.setText(SHARE_UPDATE_TEXT);
shareTextArea.cleanHighlihterDiv();
preview.clear(); preview.clear();
myLinkPreviewer = null; myLinkPreviewer = null;
} }
@ -199,6 +200,7 @@ public class ShareUpdateForm extends Composite {
submitButton.setEnabled(true); submitButton.setEnabled(true);
shareTextArea.setEnabled(true); shareTextArea.setEnabled(true);
shareTextArea.setText(SHARE_UPDATE_TEXT); shareTextArea.setText(SHARE_UPDATE_TEXT);
shareTextArea.cleanHighlihterDiv();
preview.clear(); preview.clear();
myLinkPreviewer = null; myLinkPreviewer = null;
if (feed == null) if (feed == null)
@ -232,6 +234,7 @@ public class ShareUpdateForm extends Composite {
/** /**
* Escape an html string. Escaping data received from the client helps to * Escape an html string. Escaping data received from the client helps to
* prevent cross-site script vulnerabilities. * prevent cross-site script vulnerabilities.

View File

@ -4,16 +4,22 @@
<g:HTMLPanel ui:field="mainPanel"> <g:HTMLPanel ui:field="mainPanel">
<table class="shareContainer"> <table class="shareContainer">
<tr> <tr>
<td width="60px;" align="middle"> <td width="60px;" align="middle" valign="top">
<a href=""> <a href="">
<g:Image title="Edit Profile Picture" styleName="member-photo" <g:Image title="Edit Profile Picture" styleName="member-photo"
url="" ui:field="avatarImage" width="60" height="60" /> url="" ui:field="avatarImage" width="60" height="60" />
</a> </a>
</td> </td>
<td> <td valign="top">
<div class="shareContainer-in"> <div id="supercontainer">
<m:SmartTextArea styleName="post-message" ui:field="shareTextArea" /> <div id="highlighterContainer">
<div id="highlighter">
</div>
</div>
<div id="inputContainer">
<m:SuperPosedTextArea styleName="postTextArea" ui:field="shareTextArea" />
</div>
</div> </div>
</td> </td>
</tr> </tr>
@ -25,7 +31,8 @@
<td valign="middle" width="50%"> <td valign="middle" width="50%">
<div style="text-align: left; padding-top: 2px; color: #777;"> <div style="text-align: left; padding-top: 2px; color: #777;">
privacy level: privacy level:
<g:ListBox styleName="wizardListbox" ui:field="privacyLevel" visible="false"/> <g:ListBox styleName="wizardListbox" ui:field="privacyLevel"
visible="false" />
<span id="staticPrivacyLevel"></span> <span id="staticPrivacyLevel"></span>
</div> </div>
</td> </td>

View File

@ -11,47 +11,39 @@ import org.gcube.portlets.user.pickuser.client.events.PickedUserEventHandler;
import org.gcube.portlets.user.pickuser.shared.PickingUser; import org.gcube.portlets.user.pickuser.shared.PickingUser;
import org.gcube.portlets.user.shareupdates.client.ShareUpdateService; import org.gcube.portlets.user.shareupdates.client.ShareUpdateService;
import org.gcube.portlets.user.shareupdates.client.ShareUpdateServiceAsync; import org.gcube.portlets.user.shareupdates.client.ShareUpdateServiceAsync;
import org.gcube.portlets.user.shareupdates.client.elements.ContentEditDiv;
import org.gcube.portlets.user.shareupdates.client.elements.Span;
import org.gcube.portlets.user.shareupdates.client.elements.TagBox;
import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.TextArea;
/** /**
* * @author massi
* @author Massimiliano Assante, ISTI-CNR
*
* SmartTextArea allows pasting of links with preview generation and tagging of people
* *
*/ */
public class SmartTextArea extends ContentEditDiv { public class SuperPosedTextArea extends TextArea {
/**
* Create a remote service proxy to talk to the server-side Greeting service.
*/
private final ShareUpdateServiceAsync shareupdateService = GWT private final ShareUpdateServiceAsync shareupdateService = GWT
.create(ShareUpdateService.class); .create(ShareUpdateService.class);
private final HandlerManager eventBus = new HandlerManager(null); private final HandlerManager eventBus = new HandlerManager(null);
PickUsersDialog pickUserDlg;
public final static int ARROW_UP = 38; public final static int ARROW_UP = 38;
public final static int ARROW_DOWN = 40; public final static int ARROW_DOWN = 40;
PickUsersDialog pickUserDlg;
private ArrayList<String> mentionedUsers = new ArrayList<String>();
/** /**
* *
*/ */
public SmartTextArea() { public SuperPosedTextArea() {
bind();
sinkEvents(Event.ONPASTE); sinkEvents(Event.ONPASTE);
sinkEvents(Event.ONCONTEXTMENU);
sinkEvents(Event.ONKEYUP); sinkEvents(Event.ONKEYUP);
sinkEvents(Event.ONCONTEXTMENU);
sinkEvents(Event.ONKEYDOWN); sinkEvents(Event.ONKEYDOWN);
shareupdateService.getPortalUsers(new AsyncCallback<ArrayList<PickingUser>>() { shareupdateService.getPortalUsers(new AsyncCallback<ArrayList<PickingUser>>() {
@ -65,56 +57,43 @@ public class SmartTextArea extends ContentEditDiv {
public void onFailure(Throwable caught) { public void onFailure(Throwable caught) {
} }
}); });
DOM.setElementAttribute(getElement(), "id", "postTextArea");
bind();
} }
/** /**
* events binder * @param element
*/ */
private void bind() { public SuperPosedTextArea(Element element) {
eventBus.addHandler(PickedUserEvent.TYPE, new PickedUserEventHandler() { super(element);
@Override
public void onSelectedUser(PickedUserEvent event) {
String[] toSplit = getHTML().split("@"); //get the interesting part
TagBox span = new TagBox(event.getSelectedUser().getFullName());
setHTML(toSplit[0]);
getElement().appendChild(span.getElement());
getElement().appendChild(new Span(" ").getElement());
setEndOfContenteditable();
}
});
} }
/** /**
* paste and other events overridden * paste event overridden
*/ */
public void onBrowserEvent(Event event) { public void onBrowserEvent(Event event) {
super.onBrowserEvent(event); super.onBrowserEvent(event);
switch (event.getTypeInt()) { switch (event.getTypeInt()) {
case Event.ONPASTE: { case Event.ONPASTE: {
final String before = getHTML(); final String before = getText();
GWT.log("BEFORE:" + before); GWT.log("BEFORE:" + before);
Timer t = new Timer() { Timer t = new Timer() {
@Override @Override
public void run() { public void run() {
String toCheck = extractLink(getHTML()); String toCheck = getText().replaceAll(before, "");
if (toCheck != null) { ShareUpdateForm.get().checkLink(toCheck);
GWT.log("toCheck1:" + toCheck);
ShareUpdateForm.get().checkLink(new HTML(toCheck).getText());
String sanitized = before + toCheck;
setHTML(sanitized);
setEndOfContenteditable();
}
} }
}; };
t.schedule(100); t.schedule(100);
break; break;
} }
case Event.ONCONTEXTMENU: { case Event.ONKEYUP: {
removeSampleText(); injectInDiv(getText());
pickUserDlg.onKeyUp(event.getKeyCode(), this.getAbsoluteLeft(), this.getAbsoluteTop()+65, getText());
break; break;
} }
case Event.ONKEYUP: { case Event.ONCONTEXTMENU: {
pickUserDlg.onKeyUp(event.getKeyCode(), this.getAbsoluteLeft(), this.getAbsoluteTop()+65, getText()); removeSampleText();
break; break;
} }
case Event.ONKEYDOWN: { case Event.ONKEYDOWN: {
@ -129,47 +108,61 @@ public class SmartTextArea extends ContentEditDiv {
break; break;
} }
} }
DomEvent.fireNativeEvent(event, this, this.getElement());
} }
/**
* extract the pasted link
* @param textToCheck
* @return null if there's no link present
*/
private String extractLink(String textToCheck) {
GWT.log("textToCheck:" + textToCheck);
String [] parts = textToCheck.split("\\s");
// Attempt to convert each item into an URL.
for( String item : parts ) {
if (item.startsWith("http")) {
return item;
}
if (item.startsWith("href") || item.startsWith("HREF")) { //this is for (damned) safari because sometimes it paste <a href="http... nsteand of http
textToCheck = textToCheck.replaceAll("\"", " ");
textToCheck = textToCheck.replaceAll("href", " ");
textToCheck = textToCheck.replaceAll("HREF", " ");
return extractLink(textToCheck);
}
}
//else we try plain text here (chrome)
String plainText = new HTML(textToCheck).getText();
if (textToCheck.length() != plainText.length()) {
plainText = textToCheck.replaceAll("nbsp;", " ");
String [] pparts = plainText.split("\\s");
for( String item : pparts ) {
if (item.startsWith("http"))
return item;
}
}
return null;
}
protected void removeSampleText() { protected void removeSampleText() {
if (getText().equals(ShareUpdateForm.SHARE_UPDATE_TEXT) || getText().equals(ShareUpdateForm.ERROR_UPDATE_TEXT) ) { if (getText().equals(ShareUpdateForm.SHARE_UPDATE_TEXT) || getText().equals(ShareUpdateForm.ERROR_UPDATE_TEXT) ) {
setText(""); setText("");
addStyleName("dark-color"); addStyleName("darker-color");
removeStyleName("error"); removeStyleName("error");
} }
} }
protected void cleanHighlihterDiv() {
DOM.getElementById("highlighter").setInnerHTML("");
}
private void injectInDiv(String textAreaText) {
String text;
// parse the text:
// replace all the line braks by <br/>, and all the double spaces by the html version &nbsp;
text = textAreaText.replaceAll("(\r\n|\n)","<br />");
text = text.replaceAll("\\s\\s","&nbsp;&nbsp;");
for (String mentionedUser : mentionedUsers) {
text = text.replaceAll(mentionedUser,"<span class=\"highlightedUser\">"+mentionedUser+"</span>");
}
// re-inject the processed text into the div
DOM.getElementById("highlighter").setInnerHTML(text);
}
/**
* events binder
*/
private void bind() {
eventBus.addHandler(PickedUserEvent.TYPE, new PickedUserEventHandler() {
@Override
public void onSelectedUser(PickedUserEvent event) {
String toAdd = event.getSelectedUser().getFullName();
mentionedUsers.add(toAdd);
String[] toSplit = getText().split("@"); //get the preceeding part
setText(toSplit[0]+toAdd);
Element highDiv = DOM.getElementById("highlighter");
String[] htmlToSplit = highDiv.getInnerHTML().split("@"); //get the preceeding part
String highLightedUser = "<span class=\"highlightedUser\">"+toAdd+"</span>";
highDiv.setInnerHTML(htmlToSplit[0]+highLightedUser);
}
});
}
public ArrayList<String> getMentionedUsers() {
ArrayList<String> toReturn = new ArrayList<String>();
for (String mentionedUser : mentionedUsers) {
if (getText().contains(mentionedUser))
toReturn.add(mentionedUser);
}
GWT.log(toReturn.toString());
return mentionedUsers;
}
} }

View File

@ -24,7 +24,6 @@ import org.gcube.applicationsupportlayer.social.NotificationsManager;
import org.gcube.common.core.utils.logging.GCUBEClientLog; import org.gcube.common.core.utils.logging.GCUBEClientLog;
import org.gcube.portal.custom.communitymanager.OrganizationsUtil; import org.gcube.portal.custom.communitymanager.OrganizationsUtil;
import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper; import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper;
import org.gcube.portal.databook.client.GCubeSocialNetworking;
import org.gcube.portal.databook.server.DBCassandraAstyanaxImpl; import org.gcube.portal.databook.server.DBCassandraAstyanaxImpl;
import org.gcube.portal.databook.server.DatabookStore; import org.gcube.portal.databook.server.DatabookStore;
import org.gcube.portal.databook.shared.ClientFeed; import org.gcube.portal.databook.shared.ClientFeed;
@ -52,10 +51,7 @@ import org.gcube.vomanagement.usermanagement.impl.liferay.LiferayGroupManager;
import org.gcube.vomanagement.usermanagement.impl.liferay.LiferayUserManager; import org.gcube.vomanagement.usermanagement.impl.liferay.LiferayUserManager;
import org.gcube.vomanagement.usermanagement.model.GroupModel; import org.gcube.vomanagement.usermanagement.model.GroupModel;
import org.gcube.vomanagement.usermanagement.model.UserModel; import org.gcube.vomanagement.usermanagement.model.UserModel;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlparser.beans.StringBean; import org.htmlparser.beans.StringBean;
import org.jsoup.Jsoup;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy; import org.w3c.tidy.Tidy;
@ -128,9 +124,9 @@ public class ShareUpdateServiceImpl extends RemoteServiceServlet implements Shar
/** /**
* *
*/ */
public ClientFeed share(String postText, FeedType feedType, PrivacyLevel pLevel, String vreId, String linkTitle, String linkDesc, String url, String urlThumbnail, String host) { public ClientFeed share(String postText, FeedType feedType, PrivacyLevel pLevel, String vreId, String linkTitle, String linkDesc, String url, String urlThumbnail, String host, ArrayList<String> mentionedUserFullNames) {
String escapedFeedText = transformPost(postText); String escapedFeedText = escapeHtml(postText);
ASLSession session = getASLSession(); ASLSession session = getASLSession();
String username = session.getUsername(); String username = session.getUsername();
String email = username+"@isti.cnr.it"; String email = username+"@isti.cnr.it";
@ -188,7 +184,7 @@ public class ShareUpdateServiceImpl extends RemoteServiceServlet implements Shar
toShare.getUriThumbnail(), toShare.getLinkHost()); toShare.getUriThumbnail(), toShare.getLinkHost());
//send the notification to the mentioned users //send the notification to the mentioned users
ArrayList<String> mentionedUserIds = getSelectedUserIds(extractPeopleTags(postText)); ArrayList<String> mentionedUserIds = getSelectedUserIds(mentionedUserFullNames);
if (mentionedUserIds != null && mentionedUserIds.size() > 0) { if (mentionedUserIds != null && mentionedUserIds.size() > 0) {
NotificationsManager nm = new ApplicationNotificationsManager(session); NotificationsManager nm = new ApplicationNotificationsManager(session);
Thread thread = new Thread(new MentionNotificationsThread(toShare.getKey(), escapedFeedText, nm, mentionedUserIds)); Thread thread = new Thread(new MentionNotificationsThread(toShare.getKey(), escapedFeedText, nm, mentionedUserIds));
@ -198,84 +194,6 @@ public class ShareUpdateServiceImpl extends RemoteServiceServlet implements Shar
return cf; return cf;
} }
/**
* this method is used when posting a feed
*
* It converts the tagged people <input with the actual person profile link, not trivial
*
* @param postText is the html you get from the contentDIV smartarea sth like:
* text text text <input .... value="Massimiliano Assante" ... type="text"><span> etc etc</span>
*
* @return a String ready to be posted
*/
private String transformPost(String postText) {
ArrayList<String> taggedPeople = extractPeopleTags(postText);
if (taggedPeople == null || taggedPeople.size() == 0) { //there are no tagged people, remove html and go
String escapedFeedText = escapeHtml(postText); //here escape html to avoid xss attacks
String html = "<html><head></head><body>" + escapedFeedText + "</body></html>";
return html2text(html);
} else {
_log.trace("postText curing: " + postText);
// this is needed to reconstruct the place of people tags, selfexplaining i think
int i = 0;
while (postText.contains("<input")) {
//the replacing does not affect html but affects the While guard
postText = postText.replaceFirst("<input", "_usr_place_holder_["+i+"]<br");
i++;
}
String html = "<html><head></head><body>" + postText + "</body></html>";
String postWithPlaceHolders = html2text(html);
_log.trace("before cure: " + postWithPlaceHolders);
/*
* at this point you have the html removed and somthing like "text text text _usr_place_holder_[0] text text text _usr_place_holder_[1]"
* you need to replace _usr_place_holder_[i](s) with the people with same index in taggedPeople ArrayList
*/
String escapedFeedText = escapeHtml(postWithPlaceHolders); //here escape html to avoid xss attacks
ArrayList<String> usernames = getSelectedUserIds(taggedPeople);
i = 0;
for (String tagged : taggedPeople) {
String username = (i < usernames.size()) ? usernames.get(i) : "";
String taggedHTML = "<a class=\"link\" style=\"font-size:14px;\" href=\""+GCubeSocialNetworking.USER_PROFILE_LINK+"?uid="+ username + "\">"+tagged+"</a> ";
escapedFeedText = escapedFeedText.replace("_usr_place_holder_["+i+"]", taggedHTML);
i++;
}
_log.trace("After cure: " + escapedFeedText);
return escapedFeedText;
}
}
/**
* this method extractPeopleTags from the user post
* @param postText text with tagged people: txt .. <input with the actual person profile
* @return
*/
private ArrayList<String> extractPeopleTags(String postText) {
ArrayList<String> toReturn = new ArrayList<String>();
String toParse = "<html><head></head><body>" + postText + "</body></html>";
HtmlCleaner cleaner = new HtmlCleaner();
// parse the string HTML
TagNode pageData = cleaner.clean(toParse);
TagNode[] inputElements = pageData.getElementsByName("input", true);
if (inputElements != null) {
for (int i = 0; i < inputElements.length; i++) {
System.out.println("Found input " + inputElements[i].getAttributes().get("value"));
toReturn.add(inputElements[i].getAttributes().get("value"));
}
} else {
_log.trace("No person tags in this post");
}
return toReturn;
}
/**
* remove all the html and leave the text
* @param html
* @return the text inside the html
*/
private static String html2text(String html) {
return Jsoup.parse(html).text().replace("&nbsp;"," ");
}
@ -297,13 +215,11 @@ public class ShareUpdateServiceImpl extends RemoteServiceServlet implements Shar
* @return the text with the clickable url in it * @return the text with the clickable url in it
*/ */
public String transformUrls(String feedText) { public String transformUrls(String feedText) {
System.out.println("transformUrls" + feedText);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
// separate input by spaces ( URLs have no spaces ) // separate input by spaces ( URLs have no spaces )
String [] parts = feedText.split("\\s"); String [] parts = feedText.split("\\s");
// Attempt to convert each item into an URL. // Attempt to convert each item into an URL.
for (int i = 0; i < parts.length; i++) { for (int i = 0; i < parts.length; i++) {
System.out.println("part: " + parts[i]);
if (parts[i].startsWith("http")) { if (parts[i].startsWith("http")) {
try { try {
URL url = new URL(parts[i]); URL url = new URL(parts[i]);

View File

@ -1,14 +1,88 @@
/* Superpose TextArea and Highlight DIV trick starts here */
#supercontainer {
position: relative;
}
#highlighterContainer {
position: absolute;
left: 0;
top: 0;
cursor: text;
width: 525px;
height: 54px;
}
#inputContainer {
position: relative;
}
#highlighter {
padding: 4px 2px;
color: #ffffff;
background-color: #FFF;
margin: 0px;
font-family: 'Lucida Grande', Verdana, 'Bitstream Vera Sans', Arial,
sans-serif;
font-size: 13px;
letter-spacing: normal;
line-height: normal;
border: 1px solid transparent;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
width: 525px;
height: 54px;
word-wrap: break-word; /* this is very important when usere paste long links*/
}
.postTextArea {
padding: 4px 2px;
color: #999;
background-color: transparent;
margin: 0px;
font-family: 'Lucida Grande', Verdana, 'Bitstream Vera Sans', Arial,
sans-serif;
font-size: 13px;
letter-spacing: normal;
line-height: normal;
border: 1px solid #999;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
width: 525px;
height: 54px;
}
.highlightedUser {
background-color: #D8DFEA !important;
}
/* DIV trick ends here */
.darker-color {
color: #333;
background-color: transparent;
transition: background .25s ease-in-out;
-moz-transition: background .25s ease-in-out;
-webkit-transition: background .25s ease-in-out;
}
.framed { .framed {
margin: 0 0 10px; margin: 0 0 10px;
padding: 10px; padding: 10px;
margin: 0px 5px; margin: 0px 5px;
background: #FFF url(images/vre_bg_gray.png) repeat-x left bottom; background: #FFF url(images/vre_bg_gray.png) repeat-x left bottom;
border-radius: 6px !important; border-radius: 6px !important;
-moz-border-radius: 6px !important; -moz-border-radius: 6px !important;
-webkit-border-radius: 6px !important; -webkit-border-radius: 6px !important;
border: 1px solid #DBDBDB; border: 1px solid #DBDBDB;
} }
@ -16,6 +90,7 @@
text-align: right; text-align: right;
padding-top: 2px; padding-top: 2px;
} }
.member-photo { .member-photo {
display: block; display: block;
padding: 2px; padding: 2px;
@ -34,11 +109,11 @@
} }
.hide-description { .hide-description {
margin-top: 5px; margin-top: 5px;
} }
.hide-description span label{ .hide-description span label {
padding-left: 5px; padding-left: 5px;
} }
.linkpreview-bgcolor { .linkpreview-bgcolor {
@ -125,35 +200,6 @@ a.link:hover {
padding-bottom: 3px; padding-bottom: 3px;
} }
.shareContainer-in {
padding-left: 5px;
}
.post-message {
height: 54px;
color: #999;
font-family: 'Lucida Grande', Verdana, 'Bitstream Vera Sans', Arial,
sans-serif;
font-size: 13px;
padding: 4px 2px;
width: 525px;
border: 1px solid #999;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
letter-spacing: normal;
max-height: 54px;
overflow: auto;
}
.dark-color {
color: #333;
background-color: #FFF;
transition: background .25s ease-in-out;
-moz-transition: background .25s ease-in-out;
-webkit-transition: background .25s ease-in-out;
}
.toolsContainer { .toolsContainer {
padding-top: 3px; padding-top: 3px;
width: 600px; width: 600px;
@ -189,13 +235,12 @@ a.link:hover {
} }
/* smart textarea */ /* smart textarea */
#mycontentEditableElement input[type="text"] {
#mycontentEditableElement input[type="text"] {
font-family: verdana, arial, sans-serif; font-family: verdana, arial, sans-serif;
font-size: 11px; font-size: 11px;
padding: 1px 3px; padding: 1px 3px;
color: #1C2A47; color: #1C2A47;
border: 1px solid #9DACCC; border: 1px solid #9DACCC;
border-radius: 5px; border-radius: 5px;
-moz-border-radius: 5px; -moz-border-radius: 5px;
-webkit-border-radius: 5px; -webkit-border-radius: 5px;

View File

@ -6,18 +6,30 @@
<!-- differences in layout. --> <!-- differences in layout. -->
<html> <html>
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8">
<!-- --> <!-- -->
<!-- Consider inlining CSS to reduce the number of requested files --> <!-- Consider inlining CSS to reduce the number of requested files -->
<!-- --> <!-- -->
<link type="text/css" rel="stylesheet" href="ShareUpdates.css"> <link type="text/css" rel="stylesheet" href="ShareUpdates.css">
<script type="text/javascript" language="javascript" src="shareupdates/shareupdates.nocache.js"></script> <script type="text/javascript"
</head> src="shareupdates/shareupdates.nocache.js"></script>
<script
src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js'></script>
<script src='js/jquery.autosize.js'></script>
<script>
$(document).ready(function(){
setTimeout(function(){
$('.postTextArea').autosize();
}, 3000);
});
</script>
<body> </head>
<div id="shareUpdateDiv"></div>
</body> <body>
<div id="shareUpdateDiv"></div>
</body>
</html> </html>

View File

@ -16,4 +16,14 @@
<script type="text/javascript" language="javascript" <script type="text/javascript" language="javascript"
src='<%=request.getContextPath()%>/shareupdates/shareupdates.nocache.js'></script> src='<%=request.getContextPath()%>/shareupdates/shareupdates.nocache.js'></script>
<script
src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js'></script>
<script src='<%=request.getContextPath()%>/js/jquery.autosize.js'></script>
<script>
$(document).ready(function(){
setTimeout(function(){
$('.postTextArea').autosize();
}, 3000);
});
</script>
<div id="shareUpdateDiv"></div> <div id="shareUpdateDiv"></div>

View File

@ -0,0 +1,187 @@
/*!
jQuery Autosize v1.16.12
(c) 2013 Jack Moore - jacklmoore.com
updated: 2013-05-31
license: http://www.opensource.org/licenses/mit-license.php
*/
(function ($) {
var
defaults = {
className: 'autosizejs',
append: '',
callback: false
},
hidden = 'hidden',
borderBox = 'border-box',
lineHeight = 'lineHeight',
// border:0 is unnecessary, but avoids a bug in FireFox on OSX
copy = '<textarea tabindex="-1" style="position:absolute; top:-999px; left:0; right:auto; bottom:auto; border:0; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
// line-height is conditionally included because IE7/IE8/old Opera do not return the correct value.
copyStyle = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'letterSpacing',
'textTransform',
'wordSpacing',
'textIndent'
],
oninput = 'oninput',
onpropertychange = 'onpropertychange',
// to keep track which textarea is being mirrored when adjust() is called.
mirrored,
// the mirror element, which is used to calculate what size the mirrored element should be.
mirror = $(copy).data('autosize', true)[0];
// test that line-height can be accurately copied.
mirror.style.lineHeight = '99px';
if ($(mirror).css(lineHeight) === '99px') {
copyStyle.push(lineHeight);
}
mirror.style.lineHeight = '';
$.fn.autosize = function (options) {
options = $.extend({}, defaults, options || {});
if (mirror.parentNode !== document.body) {
$(document.body).append(mirror);
}
return this.each(function () {
var
ta = this,
$ta = $(ta),
minHeight,
maxHeight,
resize,
boxOffset = 0,
callback = $.isFunction(options.callback);
if ($ta.data('autosize')) {
// exit if autosize has already been applied, or if the textarea is the mirror element.
return;
}
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox){
boxOffset = $ta.outerHeight() - $ta.height();
}
// IE8 and lower return 'auto', which parses to NaN, if no min-height is set.
minHeight = Math.max(parseInt($ta.css('minHeight'), 10) - boxOffset || 0, $ta.height());
resize = ($ta.css('resize') === 'none' || $ta.css('resize') === 'vertical') ? 'none' : 'horizontal';
$ta.css({
overflow: hidden,
overflowY: hidden,
wordWrap: 'break-word',
resize: resize
}).data('autosize', true);
function initMirror() {
mirrored = ta;
mirror.className = options.className;
maxHeight = parseInt($ta.css('maxHeight'), 10);
// mirror is a duplicate textarea located off-screen that
// is automatically updated to contain the same text as the
// original textarea. mirror always has a height of 0.
// This gives a cross-browser supported way getting the actual
// height of the text, through the scrollTop property.
$.each(copyStyle, function(i, val){
mirror.style[val] = $ta.css(val);
});
// The textarea overflow is probably now hidden, but Chrome doesn't reflow the text to account for the
// new space made available by removing the scrollbars. This workaround causes Chrome to reflow the text.
if (oninput in ta) {
var value = ta.value;
ta.value = '';
ta.value = value;
}
}
// Using mainly bare JS in this function because it is going
// to fire very often while typing, and needs to very efficient.
function adjust() {
var height, overflow, original;
if (mirrored !== ta) {
initMirror();
}
mirror.value = ta.value + options.append;
mirror.style.overflowY = ta.style.overflowY;
original = parseInt(ta.style.height,10);
// Update the width in case the original textarea width has changed
// A floor of 0 is needed because IE8 returns a negative value for hidden textareas, raising an error.
mirror.style.width = Math.max($ta.width(), 0) + 'px';
// Needed for IE8 and lower to reliably return the correct scrollTop
mirror.scrollTop = 0;
mirror.scrollTop = 9e4;
// Using scrollTop rather than scrollHeight because scrollHeight is non-standard and includes padding.
height = mirror.scrollTop;
if (maxHeight && height > maxHeight) {
height = maxHeight;
overflow = 'scroll';
} else if (height < minHeight) {
height = minHeight;
}
height += boxOffset;
ta.style.overflowY = overflow || hidden;
if (original !== height) {
ta.style.height = height + 'px';
if (callback) {
options.callback.call(ta,ta);
}
}
}
if (onpropertychange in ta) {
if (oninput in ta) {
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those occassions. There is no way that I
// know of to detect something like 'cut' in IE9.
ta[oninput] = ta.onkeyup = adjust;
} else {
// IE7 / IE8
ta[onpropertychange] = function(){
if(event.propertyName === 'value'){
adjust();
}
};
}
} else {
// Modern Browsers
ta[oninput] = adjust;
}
$(window).on('resize', function(){
active = false;
adjust();
});
// Allow for manual triggering if needed.
$ta.on('autosize', function(){
active = false;
adjust();
});
// Call adjust in case the textarea already contains text.
adjust();
});
};
}(window.jQuery || window.Zepto));