public void onBrowserEvent(Event event) { | public void onBrowserEvent(Event event) { | ||||
super.onBrowserEvent(event); | super.onBrowserEvent(event); | ||||
if (event.getTypeInt() == Event.ONCHANGE) { | if (event.getTypeInt() == Event.ONCHANGE) { | ||||
if (isImmediateMode() && fu.getFilename() != null | |||||
&& !fu.getFilename().isEmpty()) { | |||||
if (isImmediateMode() && hasFilename()) { | |||||
submit(); | submit(); | ||||
} | } | ||||
} else if (BrowserInfo.get().isIE() | } else if (BrowserInfo.get().isIE() | ||||
private boolean immediateMode; | private boolean immediateMode; | ||||
/** | |||||
* Just-in-case option to override the default assumption that if no file | |||||
* has been selected, no upload attempt should happen. Not part of public | |||||
* API so can be removed without a warning -- if you have an actual need for | |||||
* this feature (which is currently only accessible through violator | |||||
* pattern), let us know. | |||||
*/ | |||||
private boolean allowUploadWithoutFilename = false; | |||||
private String acceptMimeTypes; | private String acceptMimeTypes; | ||||
private Hidden maxfilesize = new Hidden(); | private Hidden maxfilesize = new Hidden(); | ||||
setWidget(panel); | setWidget(panel); | ||||
panel.add(maxfilesize); | panel.add(maxfilesize); | ||||
panel.add(fu); | panel.add(fu); | ||||
fu.addChangeHandler(event -> { | |||||
if (!isImmediateMode()) { | |||||
updateEnabledForSubmitButton(); | |||||
} | |||||
if (client != null) { | |||||
UploadConnector connector = ((UploadConnector) ConnectorMap | |||||
.get(client).getConnector(VUpload.this)); | |||||
if (connector.hasEventListener(EventId.CHANGE)) { | |||||
connector.getRpcProxy(UploadServerRpc.class) | |||||
.change(fu.getFilename()); | |||||
} | |||||
} | |||||
}); | |||||
submitButton = new VButton(); | submitButton = new VButton(); | ||||
submitButton.addClickHandler(event -> { | submitButton.addClickHandler(event -> { | ||||
if (isImmediateMode()) { | if (isImmediateMode()) { | ||||
fu.unsinkEvents(Event.ONCHANGE); | fu.unsinkEvents(Event.ONCHANGE); | ||||
fu.unsinkEvents(Event.ONFOCUS); | fu.unsinkEvents(Event.ONFOCUS); | ||||
} | } | ||||
updateEnabledForSubmitButton(); | |||||
} | } | ||||
setStyleName(getElement(), CLASSNAME + "-immediate", immediateMode); | setStyleName(getElement(), CLASSNAME + "-immediate", immediateMode); | ||||
} | } | ||||
/** For internal use only. May be removed or replaced in the future. */ | /** For internal use only. May be removed or replaced in the future. */ | ||||
public void disableUpload() { | public void disableUpload() { | ||||
setEnabledForSubmitButton(false); | |||||
if (!submitted) { | if (!submitted) { | ||||
// Cannot disable the fileupload while submitting or the file won't | // Cannot disable the fileupload while submitting or the file won't | ||||
// be submitted at all | // be submitted at all | ||||
fu.getElement().setPropertyBoolean("disabled", true); | fu.getElement().setPropertyBoolean("disabled", true); | ||||
} | } | ||||
enabled = false; | enabled = false; | ||||
updateEnabledForSubmitButton(); | |||||
} | } | ||||
/** For internal use only. May be removed or replaced in the future. */ | /** For internal use only. May be removed or replaced in the future. */ | ||||
public void enableUpload() { | public void enableUpload() { | ||||
setEnabledForSubmitButton(true); | |||||
fu.getElement().setPropertyBoolean("disabled", false); | fu.getElement().setPropertyBoolean("disabled", false); | ||||
enabled = true; | enabled = true; | ||||
updateEnabledForSubmitButton(); | |||||
if (submitted) { | if (submitted) { | ||||
/* | /* | ||||
* An old request is still in progress (most likely cancelled), | * An old request is still in progress (most likely cancelled), | ||||
} | } | ||||
} | } | ||||
private void setEnabledForSubmitButton(boolean enabled) { | |||||
submitButton.setEnabled(enabled); | |||||
submitButton.setStyleName(StyleConstants.DISABLED, !enabled); | |||||
/** | |||||
* Updates the enabled status for submit button. If the widget itself is | |||||
* disabled, so is the submit button. It must also follow overall enabled | |||||
* status in immediate mode, otherwise you cannot select a file at all. In | |||||
* non-immediate mode there is another button for selecting the file, so the | |||||
* submit button should be disabled until a file has been selected, unless | |||||
* upload without selection has been specifically allowed. | |||||
*/ | |||||
private void updateEnabledForSubmitButton() { | |||||
if (enabled && (isImmediateMode() || hasFilename() | |||||
|| allowUploadWithoutFilename)) { | |||||
submitButton.setEnabled(true); | |||||
submitButton.setStyleName(StyleConstants.DISABLED, false); | |||||
} else { | |||||
submitButton.setEnabled(false); | |||||
submitButton.setStyleName(StyleConstants.DISABLED, true); | |||||
} | |||||
} | |||||
private boolean hasFilename() { | |||||
return fu.getFilename() != null && !fu.getFilename().isEmpty(); | |||||
} | } | ||||
/** | /** | ||||
fu.sinkEvents(Event.ONCHANGE); | fu.sinkEvents(Event.ONCHANGE); | ||||
} | } | ||||
fu.addChangeHandler(event -> { | fu.addChangeHandler(event -> { | ||||
if (!isImmediateMode()) { | |||||
updateEnabledForSubmitButton(); | |||||
} | |||||
if (client != null) { | if (client != null) { | ||||
UploadConnector connector = ((UploadConnector) ConnectorMap | UploadConnector connector = ((UploadConnector) ConnectorMap | ||||
.get(client).getConnector(VUpload.this)); | .get(client).getConnector(VUpload.this)); | ||||
.info("Submit cancelled (disabled or already submitted)"); | .info("Submit cancelled (disabled or already submitted)"); | ||||
return; | return; | ||||
} | } | ||||
if (fu.getFilename().isEmpty()) { | |||||
if (!hasFilename()) { | |||||
if (!allowUploadWithoutFilename) { | |||||
return; | |||||
} | |||||
getLogger().info("Submitting empty selection (no file)"); | getLogger().info("Submitting empty selection (no file)"); | ||||
} | } | ||||
// flush possibly pending variable changes, so they will be handled | // flush possibly pending variable changes, so they will be handled |
import com.vaadin.client.communication.StateChangeEvent; | import com.vaadin.client.communication.StateChangeEvent; | ||||
import com.vaadin.client.ui.AbstractComponentConnector; | import com.vaadin.client.ui.AbstractComponentConnector; | ||||
import com.vaadin.client.ui.VUpload; | import com.vaadin.client.ui.VUpload; | ||||
import com.vaadin.shared.EventId; | |||||
import com.vaadin.shared.ui.Connect; | import com.vaadin.shared.ui.Connect; | ||||
import com.vaadin.shared.ui.upload.UploadClientRpc; | import com.vaadin.shared.ui.upload.UploadClientRpc; | ||||
import com.vaadin.shared.ui.upload.UploadServerRpc; | |||||
import com.vaadin.shared.ui.upload.UploadState; | import com.vaadin.shared.ui.upload.UploadState; | ||||
import com.vaadin.ui.Upload; | import com.vaadin.ui.Upload; | ||||
registerRpc(UploadClientRpc.class, () -> getWidget().submit()); | registerRpc(UploadClientRpc.class, () -> getWidget().submit()); | ||||
} | } | ||||
@Override | |||||
protected void init() { | |||||
super.init(); | |||||
getWidget().fu.addChangeHandler(event -> { | |||||
if (hasEventListener(EventId.CHANGE)) { | |||||
getRpcProxy(UploadServerRpc.class) | |||||
.change(getWidget().fu.getFilename()); | |||||
} | |||||
}); | |||||
} | |||||
@Override | @Override | ||||
public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { | public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { | ||||
if (!isRealUpdate(uidl)) { | if (!isRealUpdate(uidl)) { |
} | } | ||||
/** | /** | ||||
* Forces the upload the send selected file to the server. | |||||
* Instructs the upload component to send selected file to the server. | |||||
* <p> | * <p> | ||||
* In case developer wants to use this feature, he/she will most probably | * In case developer wants to use this feature, he/she will most probably | ||||
* want to hide the uploads internal submit button by setting its caption to | |||||
* null with {@link #setButtonCaption(String)} method. | |||||
* want to hide the upload component's internal submit button by setting its | |||||
* caption to null with {@link #setButtonCaption(String)} method. | |||||
* <p> | * <p> | ||||
* Note, that the upload runs asynchronous. Developer should use normal | |||||
* upload listeners to trac the process of upload. If the field is empty | |||||
* uploaded the file name will be empty string and file length 0 in the | |||||
* upload finished event. | |||||
* Note that the upload runs asynchronously. Developer should use normal | |||||
* upload listeners to track the process of upload. If the file name field | |||||
* is empty, no upload will be triggered. | |||||
* <p> | * <p> | ||||
* Also note, that the developer should not remove or modify the upload in | |||||
* the same user transaction where the upload submit is requested. The | |||||
* upload may safely be hidden or removed once the upload started event is | |||||
* fired. | |||||
* Also note that the developer should not remove or modify the upload | |||||
* component in the same user transaction where the upload submit is | |||||
* requested. The upload component can be safely hidden or removed once the | |||||
* upload started event has been fired. | |||||
*/ | */ | ||||
public void submitUpload() { | public void submitUpload() { | ||||
markAsDirty(); | markAsDirty(); |
import com.vaadin.shared.communication.ClientRpc; | import com.vaadin.shared.communication.ClientRpc; | ||||
/** | |||||
* Server-to-client RPC interface for Upload. | |||||
*/ | |||||
public interface UploadClientRpc extends ClientRpc { | public interface UploadClientRpc extends ClientRpc { | ||||
/** | /** | ||||
* Forces the upload the send selected file to the server. | |||||
* Instructs the upload component to send the selected file to the server. | |||||
*/ | */ | ||||
void submitUpload(); | void submitUpload(); | ||||
} | } |
import com.vaadin.shared.communication.ServerRpc; | import com.vaadin.shared.communication.ServerRpc; | ||||
/** | |||||
* Client-to-server RPC interface for Upload. | |||||
*/ | |||||
public interface UploadServerRpc extends ServerRpc { | public interface UploadServerRpc extends ServerRpc { | ||||
/** | /** |
private Upload createUpload() { | private Upload createUpload() { | ||||
Upload upload = new Upload(); | Upload upload = new Upload(); | ||||
upload.setImmediateMode(false); | |||||
upload.addChangeListener(changeListener); | upload.addChangeListener(changeListener); | ||||
return upload; | return upload; | ||||
} | } |
import java.io.ByteArrayOutputStream; | import java.io.ByteArrayOutputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import com.vaadin.annotations.Widgetset; | |||||
import com.vaadin.server.VaadinRequest; | import com.vaadin.server.VaadinRequest; | ||||
import com.vaadin.tests.components.AbstractTestUIWithLog; | import com.vaadin.tests.components.AbstractTestUIWithLog; | ||||
import com.vaadin.tests.widgetset.TestingWidgetSet; | |||||
import com.vaadin.tests.widgetset.server.upload.AllowUploadWithoutFilenameExtension; | |||||
import com.vaadin.ui.Button; | |||||
import com.vaadin.ui.Upload; | import com.vaadin.ui.Upload; | ||||
import com.vaadin.ui.Upload.Receiver; | import com.vaadin.ui.Upload.Receiver; | ||||
@Widgetset(TestingWidgetSet.NAME) | |||||
public class UploadNoSelection extends AbstractTestUIWithLog | public class UploadNoSelection extends AbstractTestUIWithLog | ||||
implements Receiver { | implements Receiver { | ||||
@Override | @Override | ||||
protected String getTestDescription() { | protected String getTestDescription() { | ||||
return "Uploading an empty selection (no file) will trigger FinishedEvent with 0-length file size and empty filename."; | |||||
return "Uploading an empty selection (no file) should not be possible by " | |||||
+ "default. If the default behavior is overridden with a custom " | |||||
+ "extension the upload attempt will trigger FinishedEvent with " | |||||
+ "0-length file size and empty filename."; | |||||
} | } | ||||
@Override | @Override | ||||
log(FILE_LENGTH_PREFIX + " " + event.getLength()); | log(FILE_LENGTH_PREFIX + " " + event.getLength()); | ||||
log(FILE_NAME_PREFIX + " " + event.getFilename()); | log(FILE_NAME_PREFIX + " " + event.getFilename()); | ||||
}); | }); | ||||
Button progButton = new Button("Upload programmatically", | |||||
e -> u.submitUpload()); | |||||
progButton.setId("programmatic"); | |||||
addComponent(progButton); | |||||
Button extButton = new Button("Allow upload without filename", | |||||
e -> AllowUploadWithoutFilenameExtension.wrap(u)); | |||||
extButton.setId("extend"); | |||||
addComponent(extButton); | |||||
} | } | ||||
@Override | @Override |
package com.vaadin.tests.widgetset.client.upload; | |||||
import com.vaadin.client.ServerConnector; | |||||
import com.vaadin.client.extensions.AbstractExtensionConnector; | |||||
import com.vaadin.client.ui.VUpload; | |||||
import com.vaadin.client.ui.upload.UploadConnector; | |||||
import com.vaadin.shared.ui.Connect; | |||||
import com.vaadin.tests.widgetset.server.upload.AllowUploadWithoutFilenameExtension; | |||||
@Connect(AllowUploadWithoutFilenameExtension.class) | |||||
public class AllowUploadWithoutFilenameConnector | |||||
extends AbstractExtensionConnector { | |||||
@Override | |||||
protected void extend(ServerConnector target) { | |||||
UploadConnector connector = ((UploadConnector) target); | |||||
allowUploadWithoutFilename(connector.getWidget()); | |||||
} | |||||
private native void allowUploadWithoutFilename(VUpload upload) | |||||
/*-{ | |||||
upload.@com.vaadin.client.ui.VUpload::allowUploadWithoutFilename = true; | |||||
}-*/; | |||||
} |
package com.vaadin.tests.widgetset.server.upload; | |||||
import com.vaadin.server.AbstractExtension; | |||||
import com.vaadin.ui.Upload; | |||||
public class AllowUploadWithoutFilenameExtension extends AbstractExtension { | |||||
public static AllowUploadWithoutFilenameExtension wrap(Upload upload) { | |||||
AllowUploadWithoutFilenameExtension extension = new AllowUploadWithoutFilenameExtension(); | |||||
extension.extend(upload); | |||||
return extension; | |||||
} | |||||
} |
package com.vaadin.tests.components.ui; | |||||
import static org.junit.Assert.assertEquals; | |||||
import static org.junit.Assert.assertTrue; | |||||
import java.io.BufferedWriter; | |||||
import java.io.File; | |||||
import java.io.FileWriter; | |||||
import java.io.IOException; | |||||
import org.junit.Test; | |||||
import org.openqa.selenium.By; | |||||
import org.openqa.selenium.WebElement; | |||||
import org.openqa.selenium.internal.WrapsElement; | |||||
import org.openqa.selenium.remote.LocalFileDetector; | |||||
import org.openqa.selenium.remote.RemoteWebElement; | |||||
import com.vaadin.testbench.elements.ButtonElement; | |||||
import com.vaadin.testbench.elements.UploadElement; | |||||
import com.vaadin.tests.tb3.MultiBrowserTest; | |||||
public class MultiFileUploadTestTest extends MultiBrowserTest { | |||||
@Test | |||||
public void changeListenerWorksAfterFirstUpload() throws IOException { | |||||
openTestURL(); | |||||
ButtonElement upload = $(ButtonElement.class).first(); | |||||
File tempFile = createTempFile(); | |||||
fillPathToUploadInput(tempFile.getPath(), | |||||
$(UploadElement.class).last()); | |||||
tempFile = createTempFile(); | |||||
fillPathToUploadInput(tempFile.getPath(), | |||||
$(UploadElement.class).last()); | |||||
tempFile = createTempFile(); | |||||
fillPathToUploadInput(tempFile.getPath(), | |||||
$(UploadElement.class).last()); | |||||
assertEquals("Unexpected amount of Upload components.", 4, | |||||
$(UploadElement.class).all().size()); | |||||
upload.click(); | |||||
// Last one doesn't have a file selected, and shouldn't trigger an | |||||
// event. | |||||
String logRow = getLogRow(0); | |||||
assertTrue("Unexpected upload log: " + logRow, | |||||
logRow.startsWith("3. Upload of ") | |||||
&& logRow.endsWith(" complete")); | |||||
} | |||||
/** | |||||
* @return The generated temp file handle | |||||
* @throws IOException | |||||
*/ | |||||
private File createTempFile() throws IOException { | |||||
File tempFile = File.createTempFile("TestFileUpload", ".txt"); | |||||
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile)); | |||||
writer.write(getTempFileContents()); | |||||
writer.close(); | |||||
tempFile.deleteOnExit(); | |||||
return tempFile; | |||||
} | |||||
private String getTempFileContents() { | |||||
StringBuilder sb = new StringBuilder("This is a small test file."); | |||||
sb.append("\n"); | |||||
sb.append("Very small."); | |||||
return sb.toString(); | |||||
} | |||||
private void fillPathToUploadInput(String tempFileName, | |||||
UploadElement uploadElement) { | |||||
// create a valid path in upload input element. Instead of selecting a | |||||
// file by some file browsing dialog, we use the local path directly. | |||||
WebElement input = getInput(uploadElement); | |||||
setLocalFileDetector(input); | |||||
input.sendKeys(tempFileName); | |||||
} | |||||
private WebElement getInput(UploadElement uploadElement) { | |||||
return uploadElement.findElement(By.className("gwt-FileUpload")); | |||||
} | |||||
private void setLocalFileDetector(WebElement element) { | |||||
if (getRunLocallyBrowser() != null) { | |||||
return; | |||||
} | |||||
if (element instanceof WrapsElement) { | |||||
element = ((WrapsElement) element).getWrappedElement(); | |||||
} | |||||
if (element instanceof RemoteWebElement) { | |||||
((RemoteWebElement) element) | |||||
.setFileDetector(new LocalFileDetector()); | |||||
} else { | |||||
throw new IllegalArgumentException( | |||||
"Expected argument of type RemoteWebElement, received " | |||||
+ element.getClass().getName()); | |||||
} | |||||
} | |||||
} |
package com.vaadin.tests.components.upload; | package com.vaadin.tests.components.upload; | ||||
import static org.junit.Assert.assertEquals; | import static org.junit.Assert.assertEquals; | ||||
import static org.junit.Assert.assertTrue; | |||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.openqa.selenium.By; | import org.openqa.selenium.By; | ||||
import org.openqa.selenium.WebElement; | import org.openqa.selenium.WebElement; | ||||
import com.vaadin.testbench.elements.ButtonElement; | |||||
import com.vaadin.tests.tb3.MultiBrowserTest; | import com.vaadin.tests.tb3.MultiBrowserTest; | ||||
public class UploadNoSelectionTest extends MultiBrowserTest { | public class UploadNoSelectionTest extends MultiBrowserTest { | ||||
// empty content is populated by com.vaadin.tests.util.Log | // empty content is populated by com.vaadin.tests.util.Log | ||||
assertEquals(" ", getLogRow(0)); | assertEquals(" ", getLogRow(0)); | ||||
getSubmitButton().click(); | |||||
WebElement submitButton = getSubmitButton(); | |||||
assertTrue("Upload button should be disabled when no selection.", | |||||
hasCssClass(submitButton, "v-disabled")); | |||||
submitButton.click(); | |||||
// clicking the disabled default button doesn't do a thing | |||||
assertEquals(" ", getLogRow(0)); | |||||
$(ButtonElement.class).id("programmatic").click(); | |||||
// neither does triggering upload programmatically | |||||
assertEquals(" ", getLogRow(0)); | |||||
// add an extension that allows upload without filename | |||||
$(ButtonElement.class).id("extend").click(); | |||||
submitButton.click(); | |||||
// expecting empty file name | // expecting empty file name | ||||
assertLogRow(0, 4, UploadNoSelection.FILE_NAME_PREFIX); | assertLogRow(0, 4, UploadNoSelection.FILE_NAME_PREFIX); | ||||
assertLogRow(1, 3, UploadNoSelection.FILE_LENGTH_PREFIX + " " + 0); | assertLogRow(1, 3, UploadNoSelection.FILE_LENGTH_PREFIX + " " + 0); | ||||
assertLogRow(2, 2, UploadNoSelection.UPLOAD_FINISHED); | assertLogRow(2, 2, UploadNoSelection.UPLOAD_FINISHED); | ||||
assertLogRow(3, 1, UploadNoSelection.RECEIVING_UPLOAD); | assertLogRow(3, 1, UploadNoSelection.RECEIVING_UPLOAD); | ||||
// and the same programmatically | |||||
$(ButtonElement.class).id("programmatic").click(); | |||||
// expecting empty file name | |||||
assertLogRow(0, 8, UploadNoSelection.FILE_NAME_PREFIX); | |||||
// expecting 0-length file | |||||
assertLogRow(1, 7, UploadNoSelection.FILE_LENGTH_PREFIX + " " + 0); | |||||
assertLogRow(2, 6, UploadNoSelection.UPLOAD_FINISHED); | |||||
assertLogRow(3, 5, UploadNoSelection.RECEIVING_UPLOAD); | |||||
} | } | ||||
private WebElement getSubmitButton() { | private WebElement getSubmitButton() { |