From d29dccac2c62f5f9b4ef7aceda1dc1cc79aad73c Mon Sep 17 00:00:00 2001 From: Costantino Perciante Date: Tue, 12 Jan 2016 18:06:10 +0000 Subject: [PATCH] Start adding support for drag and drop and multi attachment upload git-svn-id: https://svn.research-infrastructures.eu/d4science/gcube/trunk/portlets/user/share-updates@122210 82a268e6-3cf1-43bd-a215-b396298e98cf --- .gwt/.gwt-log | 0 .settings/com.google.gdt.eclipse.core.prefs | 2 +- .settings/org.eclipse.wst.common.component | 5 +- pom.xml | 5 + .../shareupdates/client/ShareUpdates.java | 28 + .../client/view/AttachedFile.java | 50 ++ .../client/view/AttachmentPreviewer.java | 126 ++++ .../client/view/AttachmentPreviewer.ui.xml | 48 ++ .../client/view/LinkPreviewer.java | 18 +- .../shareupdates/client/view/Placeholder.java | 10 +- .../client/view/ShareUpdateForm.java | 572 ++++++++++++++++-- .../user/shareupdates/shared/LinkPreview.java | 5 + src/main/webapp/ShareUpdates.css | 23 +- src/main/webapp/images/attachment_default.png | Bin 0 -> 1186 bytes src/main/webapp/images/load.png | Bin 0 -> 976 bytes src/main/webapp/images/not_load.png | Bin 0 -> 923 bytes src/main/webapp/images/reload.png | Bin 0 -> 1004 bytes 17 files changed, 800 insertions(+), 92 deletions(-) create mode 100644 .gwt/.gwt-log create mode 100644 src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachedFile.java create mode 100644 src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.java create mode 100644 src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.ui.xml create mode 100644 src/main/webapp/images/attachment_default.png create mode 100644 src/main/webapp/images/load.png create mode 100644 src/main/webapp/images/not_load.png create mode 100644 src/main/webapp/images/reload.png diff --git a/.gwt/.gwt-log b/.gwt/.gwt-log new file mode 100644 index 0000000..e69de29 diff --git a/.settings/com.google.gdt.eclipse.core.prefs b/.settings/com.google.gdt.eclipse.core.prefs index e783f74..d1c6808 100644 --- a/.settings/com.google.gdt.eclipse.core.prefs +++ b/.settings/com.google.gdt.eclipse.core.prefs @@ -1,5 +1,5 @@ eclipse.preferences.version=1 jarsExcludedFromWebInfLib= -lastWarOutDir=/Users/massi/Documents/workspace/share-updates/target/share-updates-1.6.1-SNAPSHOT +lastWarOutDir=/home/costantino/workspace/share-updates/target/share-updates-1.8.2-SNAPSHOT warSrcDir=src/main/webapp warSrcDirIsOutput=false diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component index 27d5949..8ed83f9 100644 --- a/.settings/org.eclipse.wst.common.component +++ b/.settings/org.eclipse.wst.common.component @@ -4,10 +4,7 @@ - - uses - - + uses diff --git a/pom.xml b/pom.xml index 5e9be88..6d475c5 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,11 @@ + + xerces + xercesImpl + 2.9.1 + com.google.gwt gwt-user diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/ShareUpdates.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/ShareUpdates.java index fd5e60a..2880702 100644 --- a/src/main/java/org/gcube/portlets/user/shareupdates/client/ShareUpdates.java +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/ShareUpdates.java @@ -4,16 +4,33 @@ import org.gcube.portlets.user.gcubewidgets.client.ClientScopeHelper; import org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm; import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.core.client.ScriptInjector; +import com.google.gwt.core.shared.GWT; import com.google.gwt.user.client.Window.Location; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.RootPanel; /** * Entry point classes define onModuleLoad(). + * @author Massimiliano Assante at ISTI CNR + * @author Costantino Perciante at ISTI CNR */ public class ShareUpdates implements EntryPoint { public void onModuleLoad() { + + // check if jQuery is available + boolean jQueryLoaded = isjQueryLoaded(); + + if(jQueryLoaded) + GWT.log("Injecting : http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"); + else{ + ScriptInjector.fromUrl("http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js") + .setWindow(ScriptInjector.TOP_WINDOW) + .inject(); + } + + // start UI and related stuff ClientScopeHelper.getService().setScope(Location.getHref(), new AsyncCallback() { @Override public void onSuccess(Boolean result) { @@ -24,4 +41,15 @@ public class ShareUpdates implements EntryPoint { } }); } + + /** + * Checks if jQuery is loaded. + * + * @return true, if jQuery is loaded, false otherwise + */ + private native boolean isjQueryLoaded() /*-{ + + return (typeof $wnd['jQuery'] !== 'undefined'); + + }-*/; } diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachedFile.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachedFile.java new file mode 100644 index 0000000..b2648bb --- /dev/null +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachedFile.java @@ -0,0 +1,50 @@ +package org.gcube.portlets.user.shareupdates.client.view; + +public class AttachedFile { + + private String fileName; + private String fileAbsolutePathOnServer; + private AttachmentPreviewer atPrev; + private boolean correctlyUploaded; + + /** + * + * @param fileName name of the file + * @param fileAbsolutePathOnServer path on the server + * @param atPrev object that shows such attachment + * @param uploaded has been it correctly uploaded on the server? + */ + public AttachedFile(String fileName, String fileAbsolutePathOnServer, + AttachmentPreviewer atPrev, boolean uploaded) { + super(); + this.fileName = fileName; + this.fileAbsolutePathOnServer = fileAbsolutePathOnServer; + this.atPrev = atPrev; + this.correctlyUploaded = uploaded; + } + public String getFileName() { + return fileName; + } + public void setFileName(String fileName) { + this.fileName = fileName; + } + public String getFileAbsolutePathOnServer() { + return fileAbsolutePathOnServer; + } + public void setFileAbsolutePathOnServer(String fileAbsolutePathOnServer) { + this.fileAbsolutePathOnServer = fileAbsolutePathOnServer; + } + public AttachmentPreviewer getAtPrev() { + return atPrev; + } + public void setAtPrev(AttachmentPreviewer atPrev) { + this.atPrev = atPrev; + } + public boolean isCorrectlyUploaded() { + return correctlyUploaded; + } + public void setCorrectlyUploaded(boolean correctlyUploaded) { + this.correctlyUploaded = correctlyUploaded; + } + +} diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.java new file mode 100644 index 0000000..b57e2bd --- /dev/null +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.java @@ -0,0 +1,126 @@ +package org.gcube.portlets.user.shareupdates.client.view; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; + +public class AttachmentPreviewer extends Composite { + + private static AttachmentPreviewerUiBinder uiBinder = GWT + .create(AttachmentPreviewerUiBinder.class); + + interface AttachmentPreviewerUiBinder extends + UiBinder { + } + + public AttachmentPreviewer() { + initWidget(uiBinder.createAndBindUi(this)); + } + + private static final String DELETE_ATTACHMENT = "The attachment won't be saved. Would you like to continue?"; + + @UiField + HTML deleteAttachment; + + @UiField + Image imagePreview; + + @UiField + Label fileName; + + @UiField + Label resultLabel; + + @UiField + Image resultImage; + + // Parent of this AttachmentPreviewer object + private Placeholder parent; + + // the ShareUpdateForm + private ShareUpdateForm shareUpdateForm; + + public AttachmentPreviewer(String fileName, String urlImagePreview, Placeholder parent, ShareUpdateForm shareUpdateForm) { + + initWidget(uiBinder.createAndBindUi(this)); + + // set filename and temp attachment url + this.fileName.setText(fileName); + this.imagePreview.setUrl(urlImagePreview); + + // style the delete button + this.deleteAttachment.setStyleName("su-deleteAttachment"); + this.deleteAttachment.setTitle("Cancel"); + + // save parent + this.parent = parent; + + // save the shareUpdateForm object, since it maintains the list of attached files + this.shareUpdateForm = shareUpdateForm; + } + + @UiHandler("deleteAttachment") + void onClick(ClickEvent e) { + + // alert the user + boolean confirm = Window.confirm(DELETE_ATTACHMENT); + + if(!confirm) + return; + + // we have to remove the AttachmentPreview object (that is, this object) and + // remove the file from the List of AttachedFiles + parent.remove(this); + shareUpdateForm.removeAttachedFile(this); + + } + + /** + * set the label and the that shows if the file has been saved or not + * @param result + * @param urlImageResult + */ + public void setResultAttachment(String result, String urlImageResult){ + this.resultLabel.setText(result); + this.resultImage.setUrl(urlImageResult); + } + + /** + * Change the image preview of the attachment from the default one + * @param urlImagePreview + */ + public void setImagePreview(String urlImagePreview){ + this.imagePreview.setUrl(urlImagePreview); + } + + /** + * Change style of part of this object to allow the user to retry to upload the file + * @param tooltip + * @param retryToAttachImageUrl + */ + public void setImagePreviewToRetry(String tooltip, String retryToAttachImageUrl) { + + this.imagePreview.setUrl(retryToAttachImageUrl); + this.imagePreview.setTitle(tooltip); + + // add the handler on the icon + this.imagePreview.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + Window.alert("Retry to attach handler to be implemented..."); + } + }); + + } + +} diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.ui.xml b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.ui.xml new file mode 100644 index 0000000..f1c8cc3 --- /dev/null +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/AttachmentPreviewer.ui.xml @@ -0,0 +1,48 @@ + + + + .image-preview { + align: left; + margin-right: 5px; + margin-left: 2px; + display: inline; + margin-top: 5px; + height: 40px; + } + + .attach-result { + vertical-align: top; + display: inline-block; + font-size: 10px; + font-weight: bold; + } + + .image-result { + margin-left: 4px; + width: 10px; + vertical-align: middle; + } + + .container-style { + border-style: solid; + background-color: #DCDCDC; + border-width: thin; + margin-left: 30px; + margin-top: 1px; + width: 565px; + } + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/LinkPreviewer.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/LinkPreviewer.java index 40a7b08..af24f63 100644 --- a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/LinkPreviewer.java +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/LinkPreviewer.java @@ -26,8 +26,6 @@ public class LinkPreviewer extends Composite { private LinkPreview toShow; - private SaveInWorkspaceBox saveCopy; - private boolean showImage = true; @UiField @@ -44,10 +42,8 @@ public class LinkPreviewer extends Composite { CheckBox hideCheckBox; @UiField CheckBox hideImageCheckBox; - @UiField - Placeholder uploadInWS; - public LinkPreviewer(ShareUpdateForm parent, LinkPreview toShow, boolean isFilePreview) { + public LinkPreviewer(ShareUpdateForm parent, LinkPreview toShow) { initWidget(uiBinder.createAndBindUi(this)); closeImage.setStyleName("su-closeImage"); closeImage.setTitle("Cancel"); @@ -67,10 +63,6 @@ public class LinkPreviewer extends Composite { urlText.setHTML((url.length() > 80) ? url.substring(0, 80)+"..." : url); switcher.setImages(toShow.getImageUrls()); - if (isFilePreview) { - saveCopy = new SaveInWorkspaceBox(); - uploadInWS.add(saveCopy); - } } public ImageSwitcher getSwitcher() { @@ -79,7 +71,7 @@ public class LinkPreviewer extends Composite { @UiHandler("closeImage") void onDeleteFeedClick(ClickEvent e) { - parent.cancelPreview(); + parent.cancelLinkPreview(); } @UiHandler("hideImageCheckBox") @@ -109,10 +101,4 @@ public class LinkPreviewer extends Composite { return null; return switcher.getSelectedImageURL(); } - protected boolean isSharingFile() { - return (saveCopy != null); - } - protected boolean isSaveCopySelected() { - return isSharingFile() ? saveCopy.getValue() : false; - } } diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/Placeholder.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/Placeholder.java index 0e3a2c3..69a6da9 100644 --- a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/Placeholder.java +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/Placeholder.java @@ -1,11 +1,13 @@ package org.gcube.portlets.user.shareupdates.client.view; -import com.google.gwt.user.client.ui.SimplePanel; + +import com.google.gwt.user.client.ui.VerticalPanel; /** - * - * @author massi + * This panel will contain the attachments/previews + * @author Massimiliano Assante at ISTI CNR + * @author Costantino Perciante at ISTI CNR * */ -public class Placeholder extends SimplePanel { +public class Placeholder extends VerticalPanel { } diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/ShareUpdateForm.java b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/ShareUpdateForm.java index f826a4a..641a0ba 100644 --- a/src/main/java/org/gcube/portlets/user/shareupdates/client/view/ShareUpdateForm.java +++ b/src/main/java/org/gcube/portlets/user/shareupdates/client/view/ShareUpdateForm.java @@ -1,6 +1,8 @@ package org.gcube.portlets.user.shareupdates.client.view; import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapter; import net.eliasbalasis.tibcopagebus4gwt.client.PageBusAdapterException; @@ -16,17 +18,24 @@ import org.gcube.portlets.user.shareupdates.shared.LinkPreview; import org.gcube.portlets.user.shareupdates.shared.UserSettings; import org.gcube.portlets.widgets.fileupload.client.events.FileUploadCompleteEvent; import org.gcube.portlets.widgets.fileupload.client.events.FileUploadCompleteEventHandler; +import org.gcube.portlets.widgets.fileupload.client.view.FileSubmit; import org.gcube.portlets.widgets.fileupload.client.view.UploadProgressPanel; import org.jsonmaker.gwt.client.Jsonizer; +import com.github.gwtbootstrap.client.ui.Button; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.BorderStyle; import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.FontWeight; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DragLeaveEvent; +import com.google.gwt.event.dom.client.DragLeaveHandler; +import com.google.gwt.event.dom.client.DragOverEvent; +import com.google.gwt.event.dom.client.DragOverHandler; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiFactory; @@ -34,67 +43,84 @@ import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; -import com.github.gwtbootstrap.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FileUpload; -import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.HasAlignment; -import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.ValueBoxBase.TextAlignment; import com.google.gwt.user.client.ui.Widget; /** * - * @author Massimiliano Assante + * @author Massimiliano Assante at ISTI CNR + * @author Costantino Perciante at ISTI CNR * */ public class ShareUpdateForm extends Composite { - /** - * Create a remote service proxy to talk to the server-side Greeting service. - */ + + //Create a remote service proxy to talk to the server-side Greeting service. private final ShareUpdateServiceAsync shareupdateService = GWT .create(ShareUpdateService.class); final PageBusAdapter pageBusAdapter = new PageBusAdapter(); + // the label for all Vres/channels private final static String ALL_VRES = "Share with: your Virtual Research Environments"; + // Labels protected final static String SHARE_UPDATE_TEXT = "Share an update or a link, use “@” to mention and “#” to add a topic"; protected final static String ERROR_UPDATE_TEXT = "Looks like empty to me!"; public final static String NO_TEXT_FILE_SHARE = "_N0_73X7_SH4R3_"; private final static String LISTBOX_LEVEL = " - "; + public static final String DROP_FILE_HERE_TEXT = "Drop your file(s) here!"; + public static final String ATTACHMENT_LOADED = "Attachment loaded!"; + public static final String ATTACHMENT_NOT_LOADED = "Attachment not loaded!"; + private static final String RETRY_TO_ATTACH_MESSAGE = "Retry to attach this file"; + private static final String DELETE_LINK_PREVIEW = "The link preview will be removed. Would you like to continue?"; + private static final String DELETE_ATTACHMENTS = "The attachment(s) will be removed. Would you like to continue?"; - + // image urls public static final String loading = GWT.getModuleBaseURL() + "../images/avatarLoader.gif"; public static final String avatar_default = GWT.getModuleBaseURL() + "../images/Avatar_default.png"; public static final String attach = GWT.getModuleBaseURL() + "../images/attach.png"; + public static final String attachedDefault = GWT.getModuleBaseURL() + "../images/attachment_default.png"; + public static final String loadedAttachment = GWT.getModuleBaseURL() + "../images/load.png"; + public static final String notLoadedAttachment = GWT.getModuleBaseURL() + "../images/not_load.png"; + public static final String retryToAttach = GWT.getModuleBaseURL() + "../images/reload.png"; - /** - * needed to know where to find the (possible) uploaded file - */ - private String uploadedFilePathOnServer; - private String uploadedFileNameOnServer; + // maximum number of files that can be attached! + private static final int MAX_NUMBER_ATTACHMENTS = 10; + + // remember the previous text in the textarea (while handling drag and drop) + private static String previousText; + + // list of attachedFiles (both correctly or not correctly uploaded) + private List listOfAttachedFiles = new ArrayList<>(); private HandlerManager eventBus = new HandlerManager(null); private static ShareUpdateFormUiBinder uiBinder = GWT .create(ShareUpdateFormUiBinder.class); + // The link previewer private LinkPreviewer myLinkPreviewer; + // panel that show the in progress upload of an attachment private UploadProgressPanel uploadProgress; interface ShareUpdateFormUiBinder extends UiBinder { } + // this instance private static ShareUpdateForm singleton; public static ShareUpdateForm get() { return singleton; } + @UiField HTMLPanel mainPanel; + @UiField Placeholder preview; @@ -107,7 +133,8 @@ public class ShareUpdateForm extends Composite { @UiField Image avatarImage; - @UiField SuperPosedTextArea shareTextArea; + @UiField + SuperPosedTextArea shareTextArea; @UiField ListBox privacyLevel = new ListBox(); @@ -115,23 +142,12 @@ public class ShareUpdateForm extends Composite { @UiField ListBox notifyListbox = new ListBox(); + // requested user's information private UserInfo myUserInfo; - - private void bind() { - /** - * get the uploaded file result - */ - eventBus.addHandler(FileUploadCompleteEvent.TYPE, new FileUploadCompleteEventHandler() { - @Override - public void onUploadComplete(FileUploadCompleteEvent event) { - String absolutePathOnServer = event.getUploadedFileInfo().getAbsolutePath(); - GWT.log("uploaded on Server here: " + absolutePathOnServer); - checkFile(event.getUploadedFileInfo().getFilename(), absolutePathOnServer); - } - }); - } - + /** + * Constructor + */ public ShareUpdateForm() { initWidget(uiBinder.createAndBindUi(this)); singleton = this; @@ -140,9 +156,8 @@ public class ShareUpdateForm extends Composite { shareTextArea.setText(SHARE_UPDATE_TEXT); attachButton.getElement().getStyle().setDisplay(Display.INLINE); - // attachButton.setHTML("    "); attachButton.addStyleName("upload-btn-m"); - + shareupdateService.getUserSettings(new AsyncCallback() { public void onFailure(Throwable caught) { avatarImage.setUrl(avatar_default); @@ -167,9 +182,9 @@ public class ShareUpdateForm extends Composite { privacyLevel.addItem(LISTBOX_LEVEL + "Share with: " + singleVREName, vreId); } - //privacyLevel.addItem("My Connections", PrivacyLevel.CONNECTION.toString()); if (myUserInfo.isAdmin()) privacyLevel.addItem("Share with: Everyone", PrivacyLevel.PORTAL.toString()); + //change css if deployed in VRE scope if (!userSettings.isInfrastructure()) { mainPanel.addStyleName("framed"); @@ -184,6 +199,84 @@ public class ShareUpdateForm extends Composite { privacyLevel.setVisible(true); attachButton.setVisible(true); submitButton.setVisible(true); + + // check if DND can be activated and enable it if it's possible + if(checkDNDAvailability()){ + + // add drag over handler on shareTextArea + shareTextArea.addDragOverHandler(new DragOverHandler() { + + @Override + public void onDragOver(DragOverEvent event) { + + GWT.log("Drag over handler"); + + // save current text (note that the DragOverEvent event can be fired several times) + boolean conditionToSave = !shareTextArea.getText().equals(DROP_FILE_HERE_TEXT) && !shareTextArea.getText().equals(SHARE_UPDATE_TEXT); + previousText = conditionToSave ? shareTextArea.getText() : previousText; + + // change border properties + shareTextArea.getElement().getStyle().setBorderStyle(BorderStyle.DASHED); + shareTextArea.getElement().getStyle().setBorderColor("rgba(82, 168, 236, 0.6)"); + shareTextArea.getElement().getStyle().setBorderWidth(2.5, Unit.PX); + + // change background color + shareTextArea.getElement().getStyle().setBackgroundColor("rgba(82, 168, 236, 0.2)"); + + // enlarge the window + Document.get().getElementById("highlighterContainer").getStyle().setHeight(52, Unit.PX); + Document.get().getElementById("highlighter").getStyle().setHeight(52, Unit.PX); + Document.get().getElementById("postTextArea").getStyle().setHeight(52, Unit.PX); + + // add "Drop file here" text + shareTextArea.setText(DROP_FILE_HERE_TEXT); + shareTextArea.setAlignment(TextAlignment.CENTER); + shareTextArea.getElement().getStyle().setFontWeight(FontWeight.BOLD); + shareTextArea.getElement().getStyle().setPaddingTop( + (Double.parseDouble(shareTextArea.getElement().getStyle().getHeight().replace("px", "")) + 20)/2.0, Unit.PX); + + // set the color of the text if needed to gray + if(!previousText.equals(SHARE_UPDATE_TEXT)) + shareTextArea.getElement().getStyle().setColor("#999"); + + } + }); + + // clear drag over effect + shareTextArea.addDragLeaveHandler(new DragLeaveHandler() { + + @Override + public void onDragLeave(DragLeaveEvent event) { + + GWT.log("Drag leave handler"); + + // remove style changes + resetTextArea(); + + } + + }); + + // enable shareTextArea as drop target (using native javascript) + addNativeDropHandler(singleton, FileSubmit.URL); + + } + } + }); + } + + /** + * Bind events to manage + */ + private void bind() { + + //get the uploaded file result + eventBus.addHandler(FileUploadCompleteEvent.TYPE, new FileUploadCompleteEventHandler() { + @Override + public void onUploadComplete(FileUploadCompleteEvent event) { + String absolutePathOnServer = event.getUploadedFileInfo().getAbsolutePath(); + GWT.log("uploaded on Server here: " + absolutePathOnServer); + checkFile(event.getUploadedFileInfo().getFilename(), absolutePathOnServer); } }); } @@ -208,15 +301,28 @@ public class ShareUpdateForm extends Composite { @UiHandler("attachButton") void onAttachClick(ClickEvent e) { - if (myLinkPreviewer == null) { - FileUpload up = uploadProgress.initialize(); - up.setVisible(false); - fileBrowse(up.getElement()); - uploadProgress.setVisible(true); - } else { - Window.alert("You cannot post two files, please remove the previous one first."); + + // check if there is a linkpreview + if(myLinkPreviewer != null){ + + // in this case let the user choose what to do + boolean confirm = Window.confirm(DELETE_LINK_PREVIEW); + + if(!confirm) + return; + + // remove preview + cancelLinkPreview(); } + + // proceed with the upload + FileUpload up = uploadProgress.initialize(); + up.setVisible(false); + fileBrowse(up.getElement()); + uploadProgress.setVisible(true); + } + /** * this simulates the click on the hidden native GWT FileUpload Button * @param el @@ -228,7 +334,10 @@ public class ShareUpdateForm extends Composite { @UiHandler("submitButton") void onClick(ClickEvent e) { - attachButton.getElement().getStyle().setVisibility(Visibility.VISIBLE); //beacuse otherwise it looses the other properties setting + + //because otherwise it looses the other properties setting + attachButton.getElement().getStyle().setVisibility(Visibility.VISIBLE); + shareupdateService.getUserSettings(new AsyncCallback() { public void onFailure(Throwable caught) { Window.alert("Ops! we encountered some problems delivering your message, server is not responding, please try again in a short while."); @@ -242,7 +351,7 @@ public class ShareUpdateForm extends Composite { String toShare = shareTextArea.getText().trim(); //We allow to post a file without writing nothing in the sharing textarea - if (myLinkPreviewer != null && myLinkPreviewer.isSharingFile() && (toShare.equals(SHARE_UPDATE_TEXT) || toShare.equals(ERROR_UPDATE_TEXT) || toShare.equals("")) ) { + if (myLinkPreviewer != null && (toShare.equals(SHARE_UPDATE_TEXT) || toShare.equals(ERROR_UPDATE_TEXT) || toShare.equals("")) ) { toShare = NO_TEXT_FILE_SHARE; } if (toShare.equals(SHARE_UPDATE_TEXT) || toShare.equals(ERROR_UPDATE_TEXT) || toShare.equals("")) { @@ -284,10 +393,11 @@ public class ShareUpdateForm extends Composite { linkUrl = myLinkPreviewer.getUrl(); linkUrlThumbnail = myLinkPreviewer.getUrlThumbnail(); linkHost = myLinkPreviewer.getHost(); - if (myLinkPreviewer.isSaveCopySelected()) { - fileName = uploadedFileNameOnServer; - filePath = uploadedFilePathOnServer; - } + // TODO handle attachments + // if (myLinkPreviewer.isSaveCopySelected()) { + // fileName = uploadedFileNameOnServer; + // filePath = uploadedFilePathOnServer; + // } } LinkPreview preview2Share = new LinkPreview(linkTitle, linkDescription, linkUrl, linkHost, null); boolean notifyGroup = notifyListbox.getSelectedIndex() > 0; @@ -362,6 +472,22 @@ public class ShareUpdateForm extends Composite { // Attempt to convert each item into an URL. for( String item : parts ) { if (item.startsWith("http") || item.startsWith("www")) { + + // check if there are attachments and inform the user that they will be lost + if(!listOfAttachedFiles.isEmpty()){ + + // in this case let the user to choose what to do + boolean confirm = Window.confirm(DELETE_ATTACHMENTS); + + if(!confirm) + return; + + // else... remove attachments and continue + listOfAttachedFiles.clear(); + preview.clear(); + + } + preview.add(new LinkLoader()); submitButton.setEnabled(false); //GWT.log("It's http link:" + linkToCheck); @@ -374,7 +500,7 @@ public class ShareUpdateForm extends Composite { public void onSuccess(LinkPreview result) { preview.clear(); if (result != null) - addPreview(result, false); + addPreviewLink(result); submitButton.setEnabled(true); } }); @@ -393,14 +519,26 @@ public class ShareUpdateForm extends Composite { * @param absolutePathOnServer the path of the file ending with its name on the server temp */ protected void checkFile(final String fileName, final String absolutePathOnServer) { - preview.add(new LinkLoader()); + + // create temp view of the attached file and add to the previewer + final AttachmentPreviewer atPrev = new AttachmentPreviewer(fileName, attachedDefault, preview, this); + preview.add(atPrev); + + // disable the submit button till we know the result of the upload process submitButton.setEnabled(false); + shareupdateService.checkUploadedFile(fileName, absolutePathOnServer, new AsyncCallback() { public void onFailure(Throwable caught) { - GWT.log("Failed"); + GWT.log("Upload of the file failed!"); uploadProgress.showRegisteringResult(false); - preview.clear(); + uploadProgress.setVisible(false); + + addPreviewAttachment(null, atPrev); + listOfAttachedFiles.add(new AttachedFile(fileName, absolutePathOnServer, atPrev, false)); submitButton.setEnabled(true); + + /*preview.clear(); + final HorizontalPanel hp = new HorizontalPanel(); final Button close = new Button("Try Again"); final HTML reportIssue = new HTML("" @@ -417,24 +555,23 @@ public class ShareUpdateForm extends Composite { hp.setVerticalAlignment(HasAlignment.ALIGN_MIDDLE); hp.add(close); hp.add(reportIssue); - preview.add(hp); + preview.add(hp);*/ } - public void onSuccess(LinkPreview result) { - preview.clear(); + public void onSuccess(LinkPreview result) { + + if(result == null) + return; + uploadProgress.setVisible(false); - if (result != null) - addPreview(result, true); - attachButton.getElement().getStyle().setVisibility(Visibility.HIDDEN); //beacuse otherwise it looses the other properties setting - uploadedFilePathOnServer = absolutePathOnServer; - uploadedFileNameOnServer = fileName; + addPreviewAttachment(result, atPrev); + listOfAttachedFiles.add(new AttachedFile(fileName, absolutePathOnServer, atPrev, true)); submitButton.setEnabled(true); } }); } - /** * called when pasting. it tries to avoid pasting long non spaced strings * @param linkToCheck @@ -456,17 +593,326 @@ public class ShareUpdateForm extends Composite { * add the link preview in the view * @param result */ - private void addPreview(LinkPreview result, boolean isFilePreview) { + private void addPreviewLink(LinkPreview result) { + preview.clear(); uploadProgress.setVisible(false); - myLinkPreviewer = new LinkPreviewer(this, result, isFilePreview); + myLinkPreviewer = new LinkPreviewer(this, result); preview.add(myLinkPreviewer); } + + /** + * Call it to show attachment(s) + */ + private void addPreviewAttachment(LinkPreview result, AttachmentPreviewer atPrev){ + + uploadProgress.setVisible(false); + + // check the result + if(result == null){ + // failed upload + atPrev.setResultAttachment(ATTACHMENT_NOT_LOADED, notLoadedAttachment); + + // change the preview image to reload icon to let the user retry + atPrev.setImagePreviewToRetry(RETRY_TO_ATTACH_MESSAGE, retryToAttach); + } + else{ + // set the preview information (the first image is the one related to attachments) + atPrev.setResultAttachment(ATTACHMENT_LOADED, loadedAttachment); + atPrev.setImagePreview(result.getImageUrls().get(0)); + } + + preview.add(atPrev); + } /** * */ - protected void cancelPreview() { + protected void cancelLinkPreview() { preview.clear(); myLinkPreviewer = null; attachButton.getElement().getStyle().setVisibility(Visibility.VISIBLE); //beacuse otherwise it looses the other properties setting } + + /** + * Handle drop of files within shareTextArea (native javascript code) + * @param instance + */ + private static native void addNativeDropHandler(ShareUpdateForm instance, + String servletUrl)/*-{ + + console.log("Adding drop handler to text area"); + + // retrieve textArea by id + var drop = $wnd.$('#postTextArea')[0]; + console.log("drop is " + drop); + + // check if this file is a folder + function isFolder(file) { + + if (file != null && !file.type && file.size % 4096 == 0) { + return true; + } + return false; + } + + // function used to add the handler + function addEventHandler(obj, evt, handler) { + if (obj.addEventListener) { + // W3C method + obj.addEventListener(evt, handler, false); + } else if (obj.attachEvent) { + // IE method. + obj.attachEvent('on' + evt, handler); + } else { + // Old school method. + obj['on' + evt] = handler; + } + } + + // The real drop handler + addEventHandler( + drop, + 'drop', + function(e) { + + // get window.event if e argument missing (in IE) + e = e || window.event; + + // stops the browser from redirecting off to the image. + if (e.preventDefault) { + + e.preventDefault(); + + } + + // opts for the remote call + var opts = { + + url : servletUrl, + type : "POST", + processData : false + + }; + + // get the file(s) + var dt = e.dataTransfer; + var files = dt.files; + + // check limit for number of files + var numberOfAlreadyAttachedFiles = instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::numberOfAttachments()(); + numberOfAlreadyAttachedFiles += files.length; + var limitExceeded = (files.length > @org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::MAX_NUMBER_ATTACHMENTS); + + if(limitExceeded){ + + var msg = "Too much files attached!" + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showAlert(Ljava/lang/String;)(msg); + console.log(msg); + + // reset text area + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::resetTextArea()(); + return; + } + + // reset if no file was dropped (??) + if (files.length == 0) { + + // reset text area + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::resetTextArea()(); + return; + + } + + console.log("Number of dropped file(s): " + files.length); + + var numFolder = 0; + + // save maximum allowed size + var maximumSize = @org.gcube.portlets.widgets.fileupload.client.view.FileSubmit::MAX_SIZE_ATTACHED_FILE_MB; + + // msg for ignored (too big files) + var ignoredFilesAlert = " file(s) ignored because larger than " + maximumSize + "MB"; + + // number of ignored files + var numberIgnoredFiles = 0; + + // for each dropped file + for (var i = 0; i < files.length; i++) { + + var file = files[i]; + var fileSelected = file.name + ";"; + + // be sure it is not a folder + if (!isFolder(file)) { + console.log("filesSelected: " + fileSelected); + console.log("files: " + files); + + // check its size + var fileSize = file.size / 1024 / 1024; + + console.log("File size is " + fileSize); + + if(fileSize > maximumSize){ + numberIgnoredFiles ++; + continue; + } + + // create new progress bar + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showProgressDND()(); + + // create request + var xhr = new XMLHttpRequest(); + xhr.open(opts.type, opts.url, true); + var formdata = new FormData(); + + // append the file + formdata.append("fileUpload", file); + + // send data + xhr.send(formdata); + + console.log("File " + file.name + " sent at " + servletUrl); + + }else{ + + // increment the number of skipped folders + numFolder++; + + } + } + + // alert the user that folder(s) can't be uploaded + if(numFolder > 0){ + var msg; + + if(numFolder == files.length){ + + msg = "Sorry but it's not possible to upload a folder!"; + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showAlert(Ljava/lang/String;)(msg); + + // reset text area + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::resetTextArea()(); + return; + + } + + // print ignored folders, if any + var msg = "Ignored "; + msg += numFolder > 1? numFolder+" folders": numFolder+" folder"; + msg+= " during upload."; + console.log(msg); + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showAlert(Ljava/lang/String;)(msg); + } + + // alert for too large files + if(numberIgnoredFiles > 0){ + var msg = numberIgnoredFiles + ignoredFilesAlert; + + if(numberIgnoredFiles == files.length){ + + msg = file.name + " can't be uploaded since it is too large!"; + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showAlert(Ljava/lang/String;)(msg); + + // reset text area + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::resetTextArea()(); + return; + + } + + var msg = numberIgnoredFiles + ignoredFilesAlert; + console.log(msg); + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::showAlert(Ljava/lang/String;)(msg); + } + + // reset text area + instance.@org.gcube.portlets.user.shareupdates.client.view.ShareUpdateForm::resetTextArea()(); + }); + + }-*/; + + /** + * Check if DND could be enabled (i.e, it's supported by the browser) + * @return + */ + public static native boolean checkDNDAvailability()/*-{ + + return window.FileReader; + + }-*/; + + /** + * On dragLeave reset changes on the text area + */ + private void resetTextArea() { + + // remove border properties + shareTextArea.getElement().getStyle().setBorderStyle(BorderStyle.SOLID); + shareTextArea.getElement().getStyle().setBorderColor("#333"); + shareTextArea.getElement().getStyle().setBorderWidth(1, Unit.PX); + + // change back background color + shareTextArea.getElement().getStyle().setBackgroundColor("transparent"); + + // remove text "Drop file here" and reput the old text + shareTextArea.setText(previousText); + shareTextArea.setAlignment(TextAlignment.LEFT); + + // rechange text color if needed + if(!previousText.equals(DROP_FILE_HERE_TEXT) && !previousText.equals(SHARE_UPDATE_TEXT)) + shareTextArea.getElement().getStyle().setColor("#333"); + + // reset padding top + shareTextArea.getElement().getStyle().setPaddingTop(4, Unit.PX); + + // reset font weight + shareTextArea.getElement().getStyle().setFontWeight(FontWeight.NORMAL); + } + + /** + * Alert the user about something. + * + * @param msg the msg to show + */ + private void showAlert(String msg){ + + Window.alert(msg); + + } + + /** + * Show progress bar and start the ProgressController + * @param e + */ + private void showProgressDND() { + uploadProgress.initializeDND(); + uploadProgress.setVisible(true); + } + + /** + * Remove an attached file from the listOfAttachedFiles + * @param attachmentPreviewer + */ + public void removeAttachedFile(AttachmentPreviewer attachmentPreviewer) { + + Iterator iterator = listOfAttachedFiles.iterator(); + + while (iterator.hasNext()) { + AttachedFile attachedFile = (AttachedFile) iterator.next(); + if(attachedFile.getAtPrev().equals(attachedFile)){ + iterator.remove(); + return; + } + + } + + } + + /** + * Get the number of attached files + * @return number of attached files + */ + public int numberOfAttachments(){ + + return listOfAttachedFiles.size(); + + } } diff --git a/src/main/java/org/gcube/portlets/user/shareupdates/shared/LinkPreview.java b/src/main/java/org/gcube/portlets/user/shareupdates/shared/LinkPreview.java index 5e99d68..5bbad9e 100644 --- a/src/main/java/org/gcube/portlets/user/shareupdates/shared/LinkPreview.java +++ b/src/main/java/org/gcube/portlets/user/shareupdates/shared/LinkPreview.java @@ -4,6 +4,11 @@ import java.io.Serializable; import java.util.ArrayList; @SuppressWarnings("serial") +/** + * This class is used for link preview (both actual links and attachments) + * @author Costantino Perciante at ISTI-CNR + * + */ public class LinkPreview implements Serializable { private String title; diff --git a/src/main/webapp/ShareUpdates.css b/src/main/webapp/ShareUpdates.css index d08da4f..84b8e78 100644 --- a/src/main/webapp/ShareUpdates.css +++ b/src/main/webapp/ShareUpdates.css @@ -1,4 +1,4 @@ -td > form { +td>form { margin-top: 5px !important; margin-bottom: 0px !important; } @@ -9,7 +9,7 @@ fieldset { } fieldset .control-group { - margin-bottom: 0px; + margin-bottom: 0px; } fieldset select { @@ -135,7 +135,7 @@ fieldset select { padding: 5px 15px; } -.shareButton:hover,.shareButton:focus { +.shareButton:hover, .shareButton:focus { background-color: #019AD3; background-image: linear-gradient(#33BCEF, #019AD3); border-color: #057ED0; @@ -220,6 +220,21 @@ fieldset select { cursor: hand; } +.su-deleteAttachment { + background: url(images/close.png) 0px 0px no-repeat; + height: 15px; + width: 15px; + display: inline; + float: right; + vertical-align: top; +} + +.su-deleteAttachment:hover { + background: url(images/close.png) 0px -16px no-repeat; + cursor: pointer; + cursor: hand; +} + .su-closeImage:active { background: url(images/close.png) 0px -32px no-repeat; } @@ -242,7 +257,7 @@ fieldset select { cursor: hand; } -a.link,a.link:active,a.link:visited { +a.link, a.link:active, a.link:visited { font-family: 'Lucida Grande', Verdana, 'Bitstream Vera Sans', Arial, sans-serif; font-size: 12px; diff --git a/src/main/webapp/images/attachment_default.png b/src/main/webapp/images/attachment_default.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e069519909cfc23ce4f424cfd87d36385f70f2 GIT binary patch literal 1186 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/images/reload.png b/src/main/webapp/images/reload.png new file mode 100644 index 0000000000000000000000000000000000000000..df4107da5ef2778948c928423f64f915205bcf49 GIT binary patch literal 1004 zcmVPm0V~x!*BIk3NU}VptGdirQa|9~gU#oeIL|f#LuJKJFOv$T5McwA<|nrIbMMsQ`hCz~Gt?Fm@R) z%dRoc1iC3delNQs`YHVZ3o?ir;`1ypFO^{ePDv2vP))NY2X12ixeOoRaLaYC4GT`gcB8Izj`38pCRjjmbF&Oo|smvD?clD+B-K!BZElq`IrwC*j+#bPyw znGwmnygH&=qu?$UJ-MUm+hK zOR(T+TJZ$+4~n!v6#zbVeXS7iB^;p-E%;Fv06dZUDS6kk=mgK=?_`ktW0% zEY%vIgYa?Ms|3K)OnBvC(7vTK{Z0VfRGNM#02%{=>@cW32rBXbtwFc|fKu9*2m+4` zecb@)r6dg<(jsd8>F@)92LO5!j~M?LpCz&O0PK-Y2W>V1B_QbT1Ehn{1ScpJgr-CR zX&^Ky4DLzxiBT&6;9V;(Z3Tb}@2K_xb4^NEX~Gu);KSxET*H^y3#8k#Q4oDEJ%de^s!@1kSq$Z7g`ACSxAC<|k79%9Ve`whzg5W*AZ zH64}h+iC0#ku_;ntK9tn@Xql>tNB}sKhT}rq5>*c15%m{Ck+C@!KUR_k+;+Wx`PnF zxqc){X}=Nx1udo%qEEYgGJUtnbu&aJ2vvMQl9m|ssEP244B_L^>i;|}Io-aFVm zJ|GW>3Hf^F&l4o{0_HY=hZoX!*b}G$X{z=S3rCb(KR%mQ3v6T8Bd#Y|cGP)1J>GKq aFTenI;rKPp>q9L70000