如何在鼠标点击窗口外部时关闭Vaadin子窗口?

5
我正在使用vaadin 7,在我的应用程序中有时会使用子窗口。在某些情况下,我有一个带有几个组件的模态窗口。当单击模态窗口内部某些组件时,它会打开另一个窗口。我希望当用户在其外部(例如再次单击模态窗口)单击时,此窗口能自动关闭。在Vaadin Sampler中,当显示源代码时,似乎已经实现了这种行为。而且,如果不是从模态窗口中打开,而是从UI或任何其他子窗口中打开,那么行为应该是相同的。
我尝试了几个方法:
- 使用Popupview不可行,因为我需要从组件(按钮或图像)中打开窗口 - 将BlurListener添加到新窗口中不起作用,因为如果我单击窗口内部,就会触发Blur事件(例如移动窗口) - 将ClickListener添加到UI中没有帮助,因为当单击模态窗口时,事件不会被触发
哪种方法是正确的?
谢谢
Raffael
6个回答

4

我曾遇到同样的问题,但对任何答案都不满意:

  1. @Steven Spungin 提出的聚焦/失焦方法确实有效,但仅适用于窗口内没有其他可聚焦元素的情况。否则,点击其中一个元素将关闭该窗口,这与期望相去甚远。
  2. @Steven Spungin 在他的第一个回答中提出了使用“glass element”的解决方案,但它仅适用于模态窗口,并且通常不是很灵活。
  3. @dwi wahyu utomo 提出的答案,在其中他建议添加 click 监听器与 UI.getCurrent().addClickListener 并检查单击事件坐标是否在窗口内,这确实有效,但存在一个主要问题:Vaadin 会“消耗”了鼠标点击事件并不会传播到浏览器。这意味着在页面上的任何位置单击都不会产生正常的行为,例如右键单击不会显示本机上下文菜单。

我想到了创建一个简单的 AbstractExtension,基于客户端连接器来扩展特定组件并监听页面上的所有单击事件。如果单击的目标不在扩展组件内,它就会通知服务器端连接器。

这是客户端连接器:

@Connect(ClickOutsideComponentExtension.class)
public class ClickOutsideComponentConnector extends AbstractExtensionConnector implements NativePreviewHandler {

    private ComponentConnector extendedConnector;
    private ClickOutsideComponentRpc rpc;
    private HandlerRegistration handlerRegistration;

    @Override
    protected void extend(ServerConnector target) {
        extendedConnector = (ComponentConnector) target;
        rpc = getRpcProxy(ClickOutsideComponentRpc.class);
        handlerRegistration = Event.addNativePreviewHandler(this);
    }

    @Override
    public void onUnregister() {
        super.onUnregister();
        handlerRegistration.removeHandler();
    }

    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (extendedConnector.isEnabled()) {
            Element eventTarget = Element.as(event.getNativeEvent().getEventTarget());
            if (Event.ONCLICK == event.getTypeInt() && !isElementInsideExtendedElement(eventTarget)) {
                rpc.onClickOutside();
            }
        }
    }

    public boolean isElementInsideExtendedElement(Element element) {
        Element outsideElement = extendedConnector.getWidget().getElement();
        Element insideElement = element;

        while (insideElement != null) {
            if (outsideElement.equals(insideElement)) {
                return true;
            }
            insideElement = insideElement.getParentElement();
        }
        return false;
    }

}

RPC用于客户端和服务器端之间的通信:

public interface ClickOutsideComponentRpc extends ServerRpc {
    void onClickOutside();
}

以及服务器端扩展:

public class ClickOutsideComponentExtension extends AbstractExtension {

    private List<ClickOutsideListener> clickOutsideListeners = new ArrayList<>();

    public interface ClickOutsideListener extends Serializable {
        void onClickOutside();
    }

    @Override
    public void extend(AbstractClientConnector target) {
        super.extend(target);
        registerRpc(new ClickOutsideComponentRpc() {


            @Override
            public void onClickOutside() {
                for (ClickOutsideListener listener : clickOutsideListeners) {
                    if (listener != null) {
                        listener.onClickOutside();
                    }
                }
            }
        });
    }

    public void addClickOutsideListener(ClickOutsideListener listener) {
        clickOutsideListeners.add(listener);
    }
}

如前所述,此解决方案适用于任何组件之外的点击,因此您可以执行以下操作:

Label label = new Label("Try to click outside!");
ClickOutsideComponentExtension ext = new ClickOutsideComponentExtension();
ext.extend(label);
ext.addClickOutsideListener(new ClickOutsideListener() {

    @Override
    public void onClickOutside() {
        Notification.show("Click outside of label");
    }
});
addComponent(label);

或在单击窗口之外关闭窗口:

Button btn = new Button("Open window");
btn.addClickListener(new ClickListener() {

    @Override
    public void buttonClick(ClickEvent event) {
        Window w = new Window();
        w.setContent(new Button("Focusable button"));
        w.center();
        ClickOutsideComponentExtension ext = new ClickOutsideComponentExtension();
        ext.extend(w);
        ext.addClickOutsideListener(new ClickOutsideListener() {

            @Override
            public void onClickOutside() {
                w.close();
            }
        });
        UI.getCurrent().addWindow(w);
    }
});
addComponent(btn);

我尝试了这个解决方案,但在我的程序中使用 Vaadin 7.7.x 时它并不起作用。 - Tigerware
请检查您的客户端代码(连接器和rpc)是否放置在“client”包中,否则它将无法被GWT编译,正如官方文档所述(页面底部)。此外,请确保在任何客户端代码更改后重新编译widgetset。 - mczerwi

3
如果窗口是模态的,这可能会有所帮助:
public void showWindow() {
    final Window window = new Window();
    Button closeButton = new Button("\u00a0"); // &nbsp;
    closeButton.addClickListener(new ClickListener() {
        private static final long serialVersionUID = 1L;

        @Override
        public void buttonClick(ClickEvent event) {
            window.close();
        }
    });
    closeButton.addStyleName(BaseTheme.BUTTON_LINK);
    closeButton.addStyleName("my-style");
    window.setContent(new VerticalLayout(closeButton));
    window.setModal(true);
    window.setWidth("300px");
    window.setHeight("150px");
    UI.getCurrent().addWindow(window);
}

使用以下 CSS:

.v-button.v-button-my-style {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: -5;
}

我认为这根本没有回答问题。这里的解决方案是通过单击窗口内部的某些内容来关闭窗口,然而手头的问题是如何在单击窗口外部边框时关闭窗口。 - rbaleksandar
1
@rbaleksandar:你试过这个解决方案吗?还是你只是在不理解CSS的情况下写了你的评论?因为它完全符合问题所要求的。它在我的应用程序中运行良好。 - Tigerware
@BluE 您应该意识到,1)我的评论是两年前的,2)Vaadin 与其他许多框架一样,随着时间的推移而发生变化... - rbaleksandar
@rbaleksandar 嗯,目前 Vaadin 仍存在这个问题,但这个解决方案像以前一样完美地工作。 - sukhmel

2

如果窗口的位置和大小固定,那么:

public MyWindow extends Window {
 private int x=0,y=100,width=300,height=500; 
 ...

 // constructor
 public MyWindow() {
  ...
  UI.getCurrent().addClickListener(MouseEvents.ClickListener() {
   @Override
   public void click(com.vaadin.event.MouseEvents.ClickEvent event) {
    if(!(event.getRelativeX()>=x && event.getRelativeX()<(x+width) && 
     event.getRelativeY()>=y && event.getRelativeY()<(y+height))) {
     hide();
    }
   }
  };
  ...
 }

 // showing window
 public void show() {
  if(getParent()==null) {
   setPosition(x,y);
   setWidth(width,Unit.PIXELS);
   setHeight(height,Unit.PIXELS);
   UI.getCurrent().addWindow(this);
  }
 }

 // hiding window
 public void hide() {
  if(getParent()!=null) {
   UI.getCurrent.removeWindow(this);
  }
 }

 ...
}

希望能够解决您的问题


同时,您也可以尝试在UI内部使用布局来实现相同的效果,它提供了被点击的组件。您可以使用getParent()方法搜索您的布局,然后检查它是否在窗口内。LayoutClickNotifier main = (LayoutClickNotifier) UI.getCurrent().getContent(); main.addLayoutClickListener(evt -> { Component comp = evt.getClickedComponent(); .... comp.getParent() .... }) - Grigory K

2

尝试这个:

Window window = new Window();
window.setModal(true);
window.addBlurListener(event -> window.close())

在外部点击后,您的窗口需要获得焦点才能触发失焦事件;在调用focus()之前,您需要先打开它,因为Vaadin在这方面不够体贴。

window.show();
window.focus();

0

你提到的 Vaadin Sampler 的行为依赖于非模态窗口:

 window.setModal(false); 

在这种情况下,您可以向窗口(或其下的UI/布局)添加单击侦听器,以关闭子窗口,例如。
 window.addClickListener(new MouseEvents.ClickListener() {

        @Override
        public void click(MouseEvents.ClickEvent event) {
            childWindow.close();         
        }
    });

当窗口是模态的时候,其他组件是不可访问的,因此可能没有简单的方法来定义任何在当前窗口之外触发的点击事件。


0

这里有另一种更适用于具有可聚焦控件的模态窗口的方法。

在Vaadin中打开模态窗口后,UI不会接收任何点击事件,但Window会。然而,客户端不会将玻璃点击发送到服务器。

您需要扩展Window以接收玻璃上的点击。当单击玻璃时,然后关闭窗口。

注意:我们对Vaadin创建Window元素的方式进行了假设,以便访问Glass元素。可能有更好的方法。简而言之,我们等待窗口附加,然后等待100ms以使玻璃附加,然后在玻璃上安装一个点击处理程序。

点击处理程序

public interface ClickHandler extends ServerRpc {
    void onClick(String elementId);
}

扩展器 - 服务器端

public class GlassClickExtender extends AbstractExtension {
    public GlassClickExtender(Window window, ClickHandler clickHandler) {
        extend((AbstractClientConnector) window);
        registerRpc(clickHandler);
    }
}

扩展器 - GlassClickExtenderClient

@Connect(GlassClickExtender.class)
public class GlassClickExtenderClient extends AbstractExtensionConnector {

private ClickHandler clickHandler;

@Override
protected void init() {
    clickHandler = getRpcProxy(ClickHandler.class);
    super.init();
}

@Override
protected void extend(ServerConnector serverConnector) {
    try {
        final Widget widget = ((ComponentConnector) serverConnector).getWidget();

        widget.addAttachHandler(new AttachEvent.Handler() {
            @Override
            public void onAttachOrDetach(AttachEvent event) {
                if (event.isAttached()) {
                    new Timer() {
                        @Override
                        public void run() {
                            Element windowElement = widget.getElement();
                            final Element glass = (Element) windowElement.getPreviousSibling();
                            if (glass == null || !glass.getClassName().contains("v-window-modalitycurtain")) {
                                return;
                            }
                            Event.sinkEvents(glass, Event.ONCLICK);
                            Event.setEventListener(glass, new EventListener() {

                                @Override
                                public void onBrowserEvent(Event event) {
                                    if (Event.ONCLICK == event.getTypeInt()) {
                                        clickHandler.onClick(glass.getId());
                                    }
                                }
                            });
                        }
                    }.schedule(100);
                }
                ;
            }

            ;
        });

    } catch (Exception e) {
        System.out.print(e.getMessage());
    }
}

}

现在,扩展窗口并侦听点击

 new GlassClickExtender(window, elementId -> {
                    close();
                });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接