aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtur <artur@vaadin.com>2018-03-28 15:21:16 +0300
committerIlia Motornyi <elmot@vaadin.com>2018-03-28 15:21:16 +0300
commit761bef8fb790258ac7a527f51a23da0bd1802dbe (patch)
treef7c6174a32affe0dbd9803143f09c5b9ddf959c3
parent96efeccce765a2f12db71c9af2ac07fc4283c201 (diff)
downloadvaadin-framework-761bef8fb790258ac7a527f51a23da0bd1802dbe.tar.gz
vaadin-framework-761bef8fb790258ac7a527f51a23da0bd1802dbe.zip
Support starting downloads and opening URLs from a menu item (#10478)
-rw-r--r--all/src/main/templates/release-notes.html1
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/AbstractEventTriggerExtensionConnector.java73
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/BrowserWindowOpenerConnector.java18
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/EventTrigger.java55
-rw-r--r--client/src/main/java/com/vaadin/client/extensions/FileDownloaderConnector.java22
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VMenuBar.java75
-rw-r--r--client/src/main/java/com/vaadin/client/ui/menubar/MenuBarConnector.java1
-rw-r--r--documentation/articles/LettingTheUserDownloadAFile.asciidoc2
-rw-r--r--server/src/main/java/com/vaadin/server/BrowserWindowOpener.java19
-rw-r--r--server/src/main/java/com/vaadin/server/EventTrigger.java65
-rw-r--r--server/src/main/java/com/vaadin/server/FileDownloader.java19
-rw-r--r--server/src/main/java/com/vaadin/ui/MenuBar.java57
-rw-r--r--shared/src/main/java/com/vaadin/shared/extension/PartInformationState.java33
-rw-r--r--shared/src/main/java/com/vaadin/shared/extension/filedownloader/FileDownloaderState.java4
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/BrowserWindowOpenerState.java4
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUI.java73
-rw-r--r--uitest/src/main/java/com/vaadin/tests/extensions/EventTriggerExtension.java23
-rw-r--r--uitest/src/main/java/com/vaadin/tests/widgetset/client/extension/EventTriggerExtensionConnector.java13
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUITest.java44
19 files changed, 558 insertions, 43 deletions
diff --git a/all/src/main/templates/release-notes.html b/all/src/main/templates/release-notes.html
index 8368ef4484..76c3cb2716 100644
--- a/all/src/main/templates/release-notes.html
+++ b/all/src/main/templates/release-notes.html
@@ -103,6 +103,7 @@
<h2 id="incompatible">Incompatible or Behavior-altering Changes in @version-minor@</h2>
+ <li><tt>BrowserWindowOpenerConnector</tt> and <tt>FileDownloaderConnector</tt> now are subclasses of <tt>AbstractEventTriggerExtensionConnector</tt>
<li>Date range limits in <tt>AbstractDateFieldState</tt> are now <tt>String</tt>s instead of <tt>Date</tt>s, some client-side method signatures were changed</li>
<li><tt>BrowserResizeListener</tt> is only called once resizing ends.</li>
<li><tt>ItemClickEvent</tt> for <tt>Grid</tt> now takes and additional row index parameter.</li>
diff --git a/client/src/main/java/com/vaadin/client/extensions/AbstractEventTriggerExtensionConnector.java b/client/src/main/java/com/vaadin/client/extensions/AbstractEventTriggerExtensionConnector.java
new file mode 100644
index 0000000000..29a7da9208
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/extensions/AbstractEventTriggerExtensionConnector.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.extensions;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ServerConnector;
+import com.vaadin.shared.extension.PartInformationState;
+
+/**
+ * An abstract extension connector with trigger support. Implementor's
+ * {@link #trigger} method call may be initiated by another {@code Component}
+ * without server round-trip. The class is used to overcome browser security
+ * limitations. For instance, window may not be open with the round-trip.
+ *
+ * @author Vaadin Ltd.
+ * @since
+ */
+public abstract class AbstractEventTriggerExtensionConnector
+ extends AbstractExtensionConnector {
+
+ private HandlerRegistration eventHandlerRegistration;
+
+ /**
+ * Called whenever a click occurs on the widget (if widget does not
+ * implement {@link EventTrigger}) or when the {@link EventTrigger} fires.
+ *
+ */
+ protected abstract void trigger();
+
+ @Override
+ public PartInformationState getState() {
+ return (PartInformationState) super.getState();
+ }
+
+ @Override
+ protected void extend(ServerConnector target) {
+ Widget targetWidget = ((ComponentConnector) target).getWidget();
+ if (targetWidget instanceof EventTrigger) {
+ String partInformation = getState().partInformation;
+ eventHandlerRegistration = ((EventTrigger) targetWidget)
+ .addTrigger(this::trigger, partInformation);
+ } else {
+ eventHandlerRegistration = targetWidget
+ .addDomHandler(e -> trigger(), ClickEvent.getType());
+ }
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+
+ if (eventHandlerRegistration != null) {
+ eventHandlerRegistration.removeHandler();
+ eventHandlerRegistration = null;
+ }
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/extensions/BrowserWindowOpenerConnector.java b/client/src/main/java/com/vaadin/client/extensions/BrowserWindowOpenerConnector.java
index 77844d9084..009af93f1a 100644
--- a/client/src/main/java/com/vaadin/client/extensions/BrowserWindowOpenerConnector.java
+++ b/client/src/main/java/com/vaadin/client/extensions/BrowserWindowOpenerConnector.java
@@ -18,13 +18,8 @@ package com.vaadin.client.extensions;
import java.util.Map.Entry;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.ComponentConnector;
-import com.vaadin.client.ServerConnector;
import com.vaadin.server.BrowserWindowOpener;
import com.vaadin.shared.ui.BrowserWindowOpenerState;
import com.vaadin.shared.ui.Connect;
@@ -37,15 +32,8 @@ import com.vaadin.shared.util.SharedUtil;
* @since 7.0.0
*/
@Connect(BrowserWindowOpener.class)
-public class BrowserWindowOpenerConnector extends AbstractExtensionConnector
- implements ClickHandler {
-
- @Override
- protected void extend(ServerConnector target) {
- final Widget targetWidget = ((ComponentConnector) target).getWidget();
-
- targetWidget.addDomHandler(this, ClickEvent.getType());
- }
+public class BrowserWindowOpenerConnector
+ extends AbstractEventTriggerExtensionConnector {
@Override
public BrowserWindowOpenerState getState() {
@@ -53,7 +41,7 @@ public class BrowserWindowOpenerConnector extends AbstractExtensionConnector
}
@Override
- public void onClick(ClickEvent event) {
+ protected void trigger() {
String url = getResourceUrl(BrowserWindowOpenerState.locationResource);
url = addParametersAndFragment(url);
if (url != null) {
diff --git a/client/src/main/java/com/vaadin/client/extensions/EventTrigger.java b/client/src/main/java/com/vaadin/client/extensions/EventTrigger.java
new file mode 100644
index 0000000000..eecba64515
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/extensions/EventTrigger.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.extensions;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/**
+ * Provides support for triggering an event from a given parts of a component or
+ * using various events.
+ * <p>
+ * Used by features such as {@link FileDownloaderConnector} and
+ * {@link BrowserWindowOpenerConnector} to listen to a given event on a given
+ * element. The component is the one responsible for deciding the element and
+ * the event to listen to.
+ * <p>
+ * This is the client side interface.
+ * <p>
+ * If the component on the server side implements
+ * {@code com.vaadin.server.EventTrigger} then this interface should be
+ * implemented by the {@link Widget} used by the client side connector.
+ *
+ * @since
+ */
+public interface EventTrigger {
+
+ /**
+ * Adds an appropriate event handler on the correct element inside the
+ * widget and invokes the given file downloader when the event occurs.
+ *
+ * @param command
+ * The command to execute when the event occurs
+ * @param partInformation
+ * Information passed from the server, typically telling which
+ * element to attach the DOM handler to
+ * @return a registration handler which can be used to remove the handler
+ */
+ public HandlerRegistration addTrigger(Command command,
+ String partInformation);
+
+}
diff --git a/client/src/main/java/com/vaadin/client/extensions/FileDownloaderConnector.java b/client/src/main/java/com/vaadin/client/extensions/FileDownloaderConnector.java
index d4023c299c..3edd79bde4 100644
--- a/client/src/main/java/com/vaadin/client/extensions/FileDownloaderConnector.java
+++ b/client/src/main/java/com/vaadin/client/extensions/FileDownloaderConnector.java
@@ -21,33 +21,27 @@ import com.google.gwt.dom.client.IFrameElement;
import com.google.gwt.dom.client.Style;
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.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
-import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ServerConnector;
import com.vaadin.server.FileDownloader;
import com.vaadin.shared.extension.filedownloader.FileDownloaderState;
import com.vaadin.shared.ui.Connect;
@Connect(FileDownloader.class)
-public class FileDownloaderConnector extends AbstractExtensionConnector
- implements ClickHandler {
+public class FileDownloaderConnector
+ extends AbstractEventTriggerExtensionConnector {
private IFrameElement iframe;
+ /**
+ * Called when the download should start.
+ *
+ * @since
+ */
@Override
- protected void extend(ServerConnector target) {
- final Widget downloadWidget = ((ComponentConnector) target).getWidget();
-
- downloadWidget.addDomHandler(this, ClickEvent.getType());
- }
-
- @Override
- public void onClick(ClickEvent event) {
+ protected void trigger() {
final String url = getResourceUrl("dl");
if (url != null && !url.isEmpty()) {
BrowserInfo browser = BrowserInfo.get();
diff --git a/client/src/main/java/com/vaadin/client/ui/VMenuBar.java b/client/src/main/java/com/vaadin/client/ui/VMenuBar.java
index d95954b26d..9dedb78205 100644
--- a/client/src/main/java/com/vaadin/client/ui/VMenuBar.java
+++ b/client/src/main/java/com/vaadin/client/ui/VMenuBar.java
@@ -16,8 +16,10 @@
package com.vaadin.client.ui;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Queue;
import com.google.gwt.core.client.GWT;
@@ -46,6 +48,7 @@ import com.google.gwt.user.client.ui.HasHTML;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.LayoutManager;
@@ -53,12 +56,13 @@ import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.extensions.EventTrigger;
import com.vaadin.shared.ui.ContentMode;
import com.vaadin.shared.ui.menubar.MenuBarConstants;
public class VMenuBar extends FocusableFlowPanel
-implements CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
-FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
+ implements CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
+ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler, EventTrigger {
// The hierarchy of VMenuBar is a bit weird as VMenuBar is the Paintable,
// used for the root menu but also used for the sub menus.
@@ -117,6 +121,8 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
/** For internal use only. May be removed or replaced in the future. */
public boolean htmlContentAllowed;
+ private Map<String, List<Command>> triggers = new HashMap<>();
+
public VMenuBar() {
// Create an empty horizontal menubar
this(false, null);
@@ -414,9 +420,12 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
* @param item
*/
public void itemClick(CustomMenuItem item) {
- if (item.getCommand() != null) {
+ boolean triggered = triggerEventIfNeeded(item);
+ if (item.getCommand() != null || triggered) {
try {
- item.getCommand().execute();
+ if (item.getCommand() != null) {
+ item.getCommand().execute();
+ }
} finally {
setSelected(null);
if (visibleChildMenu != null) {
@@ -808,6 +817,7 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
protected ContentMode descriptionContentMode = null;
private String styleName;
+ private String id;
/**
* Default menu item {@link Widget} constructor for GWT.create().
@@ -1166,6 +1176,14 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
return null;
}
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
}
/**
@@ -1642,6 +1660,7 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
openMenuAndFocusFirstIfPossible(getSelected());
} else {
try {
+ triggerEventIfNeeded(getSelected());
final Command command = getSelected().getCommand();
if (command != null) {
command.execute();
@@ -1654,10 +1673,7 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
// not leave menu to visible ("hover open") mode
menuVisible = false;
- VMenuBar root = this;
- while (root.getParentMenu() != null) {
- root = root.getParentMenu();
- }
+ VMenuBar root = getRoot();
root.ignoreFocus = true;
root.getElement().focus();
root.ignoreFocus = false;
@@ -1669,6 +1685,17 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
return false;
}
+ private boolean triggerEventIfNeeded(CustomMenuItem item) {
+ List<Command> commands = getTriggers().get(item.getId());
+ if (commands != null) {
+ for (Command command : commands) {
+ command.execute();
+ }
+ return true;
+ }
+ return false;
+ }
+
private void selectFirstItem() {
for (int i = 0; i < items.size(); i++) {
CustomMenuItem item = items.get(i);
@@ -1877,4 +1904,36 @@ FocusHandler, SubPartAware, MouseOutHandler, MouseOverHandler {
public void onMouseOut(MouseOutEvent event) {
LazyCloser.schedule();
}
+
+ private VMenuBar getRoot() {
+ VMenuBar root = this;
+
+ while (root.getParentMenu() != null) {
+ root = root.getParentMenu();
+ }
+
+ return root;
+ }
+
+ @Override
+ public HandlerRegistration addTrigger(Command command,
+ String partInformation) {
+ if (partInformation == null || partInformation.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The 'partInformation' parameter must contain the menu item id");
+ }
+
+ getTriggers().computeIfAbsent(partInformation, s-> new ArrayList<>()).add(command);
+ return () -> {
+ List<Command> commands = getTriggers().get(partInformation);
+ if (commands != null) {
+ commands.remove(command);
+ }
+ };
+ }
+
+ private Map<String, List<Command>> getTriggers() {
+ return getRoot().triggers;
+ }
+
}
diff --git a/client/src/main/java/com/vaadin/client/ui/menubar/MenuBarConnector.java b/client/src/main/java/com/vaadin/client/ui/menubar/MenuBarConnector.java
index 6e6869edb2..8697f07d49 100644
--- a/client/src/main/java/com/vaadin/client/ui/menubar/MenuBarConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/menubar/MenuBarConnector.java
@@ -129,6 +129,7 @@ public class MenuBarConnector extends AbstractComponentConnector
}
currentItem = currentMenu.addItem(itemHTML, cmd);
+ currentItem.setId("" + itemId);
currentItem.updateFromUIDL(item, client);
if (item.getChildCount() > 0) {
diff --git a/documentation/articles/LettingTheUserDownloadAFile.asciidoc b/documentation/articles/LettingTheUserDownloadAFile.asciidoc
index d8b381b109..48b46354a9 100644
--- a/documentation/articles/LettingTheUserDownloadAFile.asciidoc
+++ b/documentation/articles/LettingTheUserDownloadAFile.asciidoc
@@ -56,7 +56,7 @@ public class LettingUserDownladFile extends UI {
....
To use `FileDownloader`, you just create an instance of the extension
-and use it to extend the component that should start the download. You
+and use it to extend the component or `MenuItem` that should start the download. You
should also note that `FileDownloader` works best with resources that
are served by Vaadin as it relies on sending some special HTTP headers
along with the file to ensure the browser doesn't try to open the file
diff --git a/server/src/main/java/com/vaadin/server/BrowserWindowOpener.java b/server/src/main/java/com/vaadin/server/BrowserWindowOpener.java
index 838a78184a..1115acdddb 100644
--- a/server/src/main/java/com/vaadin/server/BrowserWindowOpener.java
+++ b/server/src/main/java/com/vaadin/server/BrowserWindowOpener.java
@@ -122,11 +122,30 @@ public class BrowserWindowOpener extends AbstractExtension {
setResource(BrowserWindowOpenerState.locationResource, resource);
}
+ /**
+ * Add this extension to the target component.
+ *
+ * @param target
+ * the component to attach this extension to
+ */
public void extend(AbstractComponent target) {
super.extend(target);
}
/**
+ * Add this extension to the {@code EventTrigger}.
+ *
+ * @param eventTrigger
+ * the trigger to attach this extension to
+ *
+ * @since
+ */
+ public void extend(EventTrigger eventTrigger) {
+ super.extend(eventTrigger.getConnector());
+ getState().partInformation = eventTrigger.getPartInformation();
+ }
+
+ /**
* Sets the provided URL {@code url} for this instance. The {@code url} will
* be opened in a new browser window/tab when the extended component is
* clicked.
diff --git a/server/src/main/java/com/vaadin/server/EventTrigger.java b/server/src/main/java/com/vaadin/server/EventTrigger.java
new file mode 100644
index 0000000000..08b8dbca98
--- /dev/null
+++ b/server/src/main/java/com/vaadin/server/EventTrigger.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.server;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.MenuBar;
+
+/**
+ * Provides support for triggering an event from a given parts of a component or
+ * using various events.
+ * <p>
+ * Used by features such as {@link FileDownloader} and
+ * {@link BrowserWindowOpener} to listen to a given event on a given element on
+ * the client side. The component is the one responsible for deciding the
+ * element and the event to listen to and can communicate this to the client
+ * using {@link #getPartInformation()}.
+ * <p>
+ * This is the server side interface.
+ * <p>
+ * If a {@link Component} implements this interface, then the corresponding
+ * connector on the client side must implement
+ * {@code com.vaadin.client.extensions.EventTrigger}.
+ *
+ * @since
+ */
+public interface EventTrigger extends Serializable {
+
+ /**
+ * Gets the connector who will be used to offer the file download. Typically
+ * a component containing a certain DOM element, which in turn triggers the
+ * download.
+ *
+ * @return the connector for the file download
+ */
+ AbstractClientConnector getConnector();
+
+ /**
+ * Gets a free form string which identifies which part of the connector that
+ * should trigger the download. The string is passed to the connector
+ * (FileDownloaderHandler implementor) on the client side.
+ * <p>
+ * For example, {@link MenuBar} passes the id of a menu item through this
+ * method so that the client side can listen to events for that particular
+ * item only.
+ *
+ * @return a free form string which makes sense to the client side connector
+ */
+ String getPartInformation();
+
+}
diff --git a/server/src/main/java/com/vaadin/server/FileDownloader.java b/server/src/main/java/com/vaadin/server/FileDownloader.java
index a35278bd9d..df4240f368 100644
--- a/server/src/main/java/com/vaadin/server/FileDownloader.java
+++ b/server/src/main/java/com/vaadin/server/FileDownloader.java
@@ -66,11 +66,30 @@ public class FileDownloader extends AbstractExtension {
setResource("dl", resource);
}
+ /**
+ * Add this extension to the target component.
+ *
+ * @param target
+ * the component to attach this extension to
+ */
public void extend(AbstractComponent target) {
super.extend(target);
}
/**
+ * Add this extension to the {@code EventTrigger}.
+ *
+ * @param eventTrigger
+ * the trigger to attach this extension to
+ *
+ * @since
+ */
+ public void extend(EventTrigger eventTrigger) {
+ super.extend(eventTrigger.getConnector());
+ getState().partInformation = eventTrigger.getPartInformation();
+ }
+
+ /**
* Gets the resource set for download.
*
* @return the resource that will be downloaded if clicking the extended
diff --git a/server/src/main/java/com/vaadin/ui/MenuBar.java b/server/src/main/java/com/vaadin/ui/MenuBar.java
index f6bafb5d15..4f7055bf6e 100644
--- a/server/src/main/java/com/vaadin/ui/MenuBar.java
+++ b/server/src/main/java/com/vaadin/ui/MenuBar.java
@@ -28,6 +28,8 @@ import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.parser.Tag;
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.EventTrigger;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.server.Resource;
@@ -218,6 +220,23 @@ implements LegacyComponent, Focusable {
}
/**
+ * Adds a new menu item to the menu bar
+ * <p>
+ * Clicking on this menu item has no effect. Use
+ * {@link #addItem(String, Command)} or {@link MenuItem#setCommand(Command)}
+ * to assign an action to the menu item.
+ *
+ * @param caption
+ * the text for the menu item
+ * @throws IllegalArgumentException
+ *
+ * @since
+ */
+ public MenuBar.MenuItem addItem(String caption) {
+ return addItem(caption, null, null);
+ }
+
+ /**
* Add a new item to the menu bar. Command can be null, but a caption must
* be given.
*
@@ -453,7 +472,7 @@ implements LegacyComponent, Focusable {
* multiple MenuItems to a MenuItem and create a sub-menu.
*
*/
- public class MenuItem implements Serializable {
+ public class MenuItem implements Serializable, EventTrigger {
/** Private members * */
private final int itsId;
@@ -523,6 +542,22 @@ implements LegacyComponent, Focusable {
}
/**
+ * Add a new menu item inside this menu item, creating a sub-menu.
+ * <p>
+ * Clicking on the new item has no effect. Use
+ * {@link #addItem(String, Command)} or {@link #setCommand(Command)} to
+ * assign an action to the menu item.
+ *
+ * @param caption
+ * the text for the menu item
+ *
+ * @since
+ */
+ public MenuBar.MenuItem addItem(String caption) {
+ return addItem(caption, null, null);
+ }
+
+ /**
* Add a new item inside this item, thus creating a sub-menu. Command
* can be null, but a caption must be given.
*
@@ -992,6 +1027,26 @@ implements LegacyComponent, Focusable {
this.checked = checked;
markAsDirty();
}
+
+ /**
+ * Gets the menu bar this item is part of.
+ *
+ * @return the menu bar this item is attached to
+ * @since
+ */
+ public MenuBar getMenuBar() {
+ return MenuBar.this;
+ }
+
+ @Override
+ public AbstractClientConnector getConnector() {
+ return getMenuBar();
+ }
+
+ @Override
+ public String getPartInformation() {
+ return String.valueOf(getId());
+ }
}
@Override
diff --git a/shared/src/main/java/com/vaadin/shared/extension/PartInformationState.java b/shared/src/main/java/com/vaadin/shared/extension/PartInformationState.java
new file mode 100644
index 0000000000..27707fde72
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/extension/PartInformationState.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2018 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.shared.extension;
+
+import com.vaadin.shared.communication.SharedState;
+
+/**
+ * Shared state for {@code AbstractEventTriggerExtensionConnector} extension.
+ *
+ * @since
+ */
+public class PartInformationState extends SharedState {
+
+ /**
+ * Information passed to the widget on the client side, to allow it to
+ * attach to the correct DOM element.
+ */
+ public String partInformation;
+
+}
diff --git a/shared/src/main/java/com/vaadin/shared/extension/filedownloader/FileDownloaderState.java b/shared/src/main/java/com/vaadin/shared/extension/filedownloader/FileDownloaderState.java
index 847fb7548c..f7286b551c 100644
--- a/shared/src/main/java/com/vaadin/shared/extension/filedownloader/FileDownloaderState.java
+++ b/shared/src/main/java/com/vaadin/shared/extension/filedownloader/FileDownloaderState.java
@@ -15,7 +15,7 @@
*/
package com.vaadin.shared.extension.filedownloader;
-import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.extension.PartInformationState;
/**
* Shared state for FileDownloader.
@@ -24,6 +24,6 @@ import com.vaadin.shared.communication.SharedState;
*
* @since 8.0
*/
-public class FileDownloaderState extends SharedState {
+public class FileDownloaderState extends PartInformationState {
}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/BrowserWindowOpenerState.java b/shared/src/main/java/com/vaadin/shared/ui/BrowserWindowOpenerState.java
index dce96d6b18..c3bfc2ff0a 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/BrowserWindowOpenerState.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/BrowserWindowOpenerState.java
@@ -19,9 +19,9 @@ package com.vaadin.shared.ui;
import java.util.HashMap;
import java.util.Map;
-import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.extension.PartInformationState;
-public class BrowserWindowOpenerState extends SharedState {
+public class BrowserWindowOpenerState extends PartInformationState {
public static final String locationResource = "url";
public String target = "_blank";
diff --git a/uitest/src/main/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUI.java b/uitest/src/main/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUI.java
new file mode 100644
index 0000000000..27ab5d2347
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUI.java
@@ -0,0 +1,73 @@
+package com.vaadin.tests.components;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.BrowserWindowOpener;
+import com.vaadin.server.ClassResource;
+import com.vaadin.server.ConnectorResource;
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.FileDownloader;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.embedded.EmbeddedPdf;
+import com.vaadin.tests.extensions.EventTriggerExtension;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.MenuBar;
+import com.vaadin.ui.MenuBar.MenuItem;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class MenuBarDownloadBrowserOpenerUI extends AbstractTestUIWithLog {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ ConnectorResource downloadResource = new ClassResource(
+ EmbeddedPdf.class, "test.pdf");
+ ExternalResource openResource = new ExternalResource(
+ "https://vaadin.com");
+
+ MenuBar menuBar = new MenuBar();
+ MenuItem download = menuBar.addItem("Download");
+ MenuItem saveAsNoLog = download.addItem("Save as without logging...");
+ MenuItem saveAsLog = download.addItem("Save as with logging...",
+ item -> log("Download triggered"));
+ FileDownloader fd = new FileDownloader(downloadResource);
+ fd.extend(saveAsNoLog);
+ FileDownloader fd2 = new FileDownloader(downloadResource);
+ fd2.extend(saveAsLog);
+
+ MenuItem open = menuBar.addItem("Open");
+ MenuItem openNoLog = open.addItem("Open without logging...");
+ MenuItem openLog = open.addItem("Open with logging...", item -> log("Open triggered"));
+
+ BrowserWindowOpener bwo = new BrowserWindowOpener(openResource);
+ bwo.extend(openNoLog);
+ BrowserWindowOpener bwo2 = new BrowserWindowOpener(openResource);
+ bwo2.extend(openLog);
+
+ addComponent(menuBar);
+
+ addComponent(new Button("Remove downloaders and openers", event -> {
+ fd.remove();
+ fd2.remove();
+ bwo.remove();
+ bwo2.remove();
+ }));
+
+ setupTestExtension(menuBar);
+
+ }
+
+ private void setupTestExtension(MenuBar menuBar) {
+ EventTriggerExtension triggerable1 = new EventTriggerExtension();
+ EventTriggerExtension triggerable2 = new EventTriggerExtension();
+
+ MenuItem testExtension = menuBar.addItem("TestExtension");
+ MenuItem runMe = testExtension.addItem("RunMe");
+ triggerable1.extend(runMe);
+
+ testExtension.addItem("AddTrigger", c -> triggerable2.extend(runMe));
+ testExtension.addItem("RemoveTrigger", c -> triggerable2.remove());
+
+ }
+
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/extensions/EventTriggerExtension.java b/uitest/src/main/java/com/vaadin/tests/extensions/EventTriggerExtension.java
new file mode 100644
index 0000000000..c6bac2b54a
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/extensions/EventTriggerExtension.java
@@ -0,0 +1,23 @@
+package com.vaadin.tests.extensions;
+
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.EventTrigger;
+import com.vaadin.shared.extension.PartInformationState;
+
+public class EventTriggerExtension extends AbstractExtension {
+
+ @Override
+ protected PartInformationState getState() {
+ return (PartInformationState) super.getState();
+ }
+
+ @Override
+ protected PartInformationState getState(boolean markAsDirty) {
+ return (PartInformationState) super.getState(markAsDirty);
+ }
+
+ public void extend(EventTrigger eventTrigger) {
+ super.extend(eventTrigger.getConnector());
+ getState().partInformation = eventTrigger.getPartInformation();
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/widgetset/client/extension/EventTriggerExtensionConnector.java b/uitest/src/main/java/com/vaadin/tests/widgetset/client/extension/EventTriggerExtensionConnector.java
new file mode 100644
index 0000000000..10994f7430
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/widgetset/client/extension/EventTriggerExtensionConnector.java
@@ -0,0 +1,13 @@
+package com.vaadin.tests.widgetset.client.extension;
+
+import com.vaadin.client.extensions.AbstractEventTriggerExtensionConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.extensions.EventTriggerExtension;
+
+@Connect(EventTriggerExtension.class)
+public class EventTriggerExtensionConnector extends AbstractEventTriggerExtensionConnector{
+ @Override
+ protected native void trigger() /*-{
+ alert("Trigger");
+ }-*/;
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUITest.java b/uitest/src/test/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUITest.java
new file mode 100644
index 0000000000..00e12bd1f3
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/MenuBarDownloadBrowserOpenerUITest.java
@@ -0,0 +1,44 @@
+package com.vaadin.tests.components;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.Alert;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.elements.MenuBarElement;
+import com.vaadin.testbench.parallel.Browser;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class MenuBarDownloadBrowserOpenerUITest extends MultiBrowserTest {
+ @Override
+ public List<DesiredCapabilities> getBrowsersToTest() {
+ //alerts do not work properly on PhantomJS
+ return getBrowserCapabilities(Browser.CHROME);
+ }
+
+ @Test
+ public void testTriggerExtension() {
+ openTestURL();
+ MenuBarElement first = $(MenuBarElement.class).first();
+ first.clickItem("TestExtension", "RunMe");
+ checkAndCloseAlert();
+
+ first.clickItem("TestExtension", "AddTrigger");
+ first.clickItem("TestExtension", "RunMe");
+ checkAndCloseAlert();
+ checkAndCloseAlert();
+
+ first.clickItem("TestExtension", "RemoveTrigger");
+ first.clickItem("TestExtension", "RunMe");
+ checkAndCloseAlert();
+ }
+
+ private void checkAndCloseAlert() {
+ Alert alert = getDriver().switchTo().alert();
+ Assert.assertEquals("Trigger",alert.getText());
+ alert.dismiss();
+ }
+
+}