com.vaadin.client.ui.VNotification.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.client.ui.VNotification.java

Source

/*
 * 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.ui;

import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;

import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.AnimationUtil;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.aria.AriaHelper;
import com.vaadin.shared.Position;
import com.vaadin.shared.ui.ui.NotificationRole;
import com.vaadin.shared.ui.ui.UIState.NotificationTypeConfiguration;

public class VNotification extends VOverlay {

    public static final Position CENTERED = Position.MIDDLE_CENTER;
    public static final Position CENTERED_TOP = Position.TOP_CENTER;
    public static final Position CENTERED_BOTTOM = Position.BOTTOM_CENTER;
    public static final Position TOP_LEFT = Position.TOP_LEFT;
    public static final Position TOP_RIGHT = Position.TOP_RIGHT;
    public static final Position BOTTOM_LEFT = Position.BOTTOM_LEFT;
    public static final Position BOTTOM_RIGHT = Position.BOTTOM_RIGHT;

    private static final String STYLENAME_POSITION_TOP = "v-position-top";
    private static final String STYLENAME_POSITION_RIGHT = "v-position-right";
    private static final String STYLENAME_POSITION_BOTTOM = "v-position-bottom";
    private static final String STYLENAME_POSITION_LEFT = "v-position-left";
    private static final String STYLENAME_POSITION_MIDDLE = "v-position-middle";
    private static final String STYLENAME_POSITION_CENTER = "v-position-center";
    private static final String STYLENAME_POSITION_ASSISTIVE = "v-position-assistive";

    public static final String CAPTION = "caption";
    public static final String DESCRIPTION = "description";
    public static final String DETAILS = "details";

    /**
     * Position that is only accessible for assistive devices, invisible for
     * visual users.
     */
    public static final Position ASSISTIVE = Position.ASSISTIVE;

    public static final int DELAY_FOREVER = -1;
    public static final int DELAY_NONE = 0;

    private static final String STYLENAME = "v-Notification";
    private static final int MOUSE_MOVE_THRESHOLD = 7;
    private static final int Z_INDEX_BASE = 20000;
    public static final String STYLE_SYSTEM = "system";

    private static final List<VNotification> NOTIFICATIONS = new ArrayList<>();

    private boolean infiniteDelay = false;
    private int hideDelay = 0;

    private Timer delay;

    private int x = -1;
    private int y = -1;

    private String temporaryStyle;

    private List<EventListener> listeners;
    private static final int TOUCH_DEVICE_IDLE_DELAY = 1000;

    /**
     * Default constructor. You should use GWT.create instead.
     */
    public VNotification() {
        setStyleName(STYLENAME);
        sinkEvents(Event.ONCLICK);
        getElement().getStyle().setZIndex(Z_INDEX_BASE);
    }

    /**
     * @deprecated Use static {@link #createNotification(int)} instead to enable
     *             GWT deferred binding.
     *
     * @param delayMsec
     */
    @Deprecated
    public VNotification(int delayMsec) {
        this();
        setDelay(delayMsec);

        if (BrowserInfo.get().isTouchDevice()) {
            new Timer() {
                @Override
                public void run() {
                    if (isAttached()) {
                        hide();
                    }
                }
            }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY);
        }
    }

    /**
     * @deprecated Use static {@link #createNotification(int, int, int)} instead
     *             to enable GWT deferred binding.
     *
     * @param delayMsec
     * @param fadeMsec
     * @param startOpacity
     */
    @Deprecated
    public VNotification(int delayMsec, int fadeMsec, int startOpacity) {
        this(delayMsec);
        AnimationUtil.setAnimationDuration(getElement(), fadeMsec + "ms");
        getElement().getStyle().setOpacity(startOpacity / 100);
    }

    private void setDelay(int delayMsec) {
        if (delayMsec < 0) {
            infiniteDelay = true;
            hideDelay = 0;
        } else {
            infiniteDelay = false;
            hideDelay = delayMsec;
        }
    }

    @Override
    public void show() {
        show(CENTERED);
    }

    public void show(String style) {
        show(CENTERED, style);
    }

    public void show(com.vaadin.shared.Position position) {
        show(position, null);
    }

    public void show(Widget widget, Position position, String style) {
        NotificationTypeConfiguration styleSetup = getUiState(style);
        setWaiAriaRole(styleSetup);

        FlowPanel panel = new FlowPanel();
        if (hasPrefix(styleSetup)) {
            panel.add(new Label(styleSetup.prefix));
            AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(), true);
        }

        panel.add(widget);

        if (hasPostfix(styleSetup)) {
            panel.add(new Label(styleSetup.postfix));
            AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(), true);
        }
        setWidget(panel);
        show(position, style);
    }

    private boolean hasPostfix(NotificationTypeConfiguration styleSetup) {
        return styleSetup != null && styleSetup.postfix != null && !styleSetup.postfix.isEmpty();
    }

    private boolean hasPrefix(NotificationTypeConfiguration styleSetup) {
        return styleSetup != null && styleSetup.prefix != null && !styleSetup.prefix.isEmpty();
    }

    public void show(String html, Position position, String styleName) {
        NotificationTypeConfiguration styleSetup = getUiState(styleName);
        String assistiveDeviceOnlyStyle = AriaHelper.ASSISTIVE_DEVICE_ONLY_STYLE;

        setWaiAriaRole(styleSetup);

        String type = "";
        String usage = "";

        if (hasPrefix(styleSetup)) {
            type = "<span class='" + assistiveDeviceOnlyStyle + "'>" + styleSetup.prefix + "</span>";
        }

        if (hasPostfix(styleSetup)) {
            usage = "<span class='" + assistiveDeviceOnlyStyle + "'>" + styleSetup.postfix + "</span>";
        }

        setWidget(new HTML(type + html + usage));
        show(position, styleName);
    }

    private NotificationTypeConfiguration getUiState(String style) {
        if (getApplicationConnection() == null || getApplicationConnection().getUIConnector() == null) {
            return null;
        }

        return getApplicationConnection().getUIConnector().getState().notificationConfigurations.get(style);
    }

    private void setWaiAriaRole(NotificationTypeConfiguration styleSetup) {
        Roles.getAlertRole().set(getElement());

        if (styleSetup != null && styleSetup.notificationRole != null) {
            if (NotificationRole.STATUS == styleSetup.notificationRole) {
                Roles.getStatusRole().set(getElement());
            }
        }
    }

    public void show(Position position, String style) {
        if (temporaryStyle != null) {
            removeStyleName(temporaryStyle);
            removeStyleDependentName(temporaryStyle);
            temporaryStyle = null;
        }
        if (style != null && !style.isEmpty()) {
            temporaryStyle = style;
            addStyleName(style);
            addStyleDependentName(style);
        }

        setPosition(position);
        super.show();
        updatePositionOffsets(position);
        NOTIFICATIONS.add(this);
        positionOrSizeUpdated();
        /**
         * Android 4 fails to render notifications correctly without a little
         * nudge (#8551) Chrome 41 now requires this too (#17252)
         */
        if (BrowserInfo.get().isAndroid() || isChrome41OrHigher()) {
            WidgetUtil.setStyleTemporarily(getElement(), "display", "none");
        }
    }

    private boolean isChrome41OrHigher() {
        return BrowserInfo.get().isChrome() && BrowserInfo.get().getBrowserMajorVersion() >= 41;
    }

    protected void hideAfterDelay() {
        if (delay == null) {
            delay = new Timer() {
                @Override
                public void run() {
                    VNotification.super.hide();
                }
            };
            delay.schedule(hideDelay);
        }
    }

    @Override
    public void hide() {
        if (delay != null) {
            delay.cancel();
        }
        // Run only once
        if (NOTIFICATIONS.contains(this)) {
            DOM.removeEventPreview(this);

            // Still animating in, wait for it to finish before touching
            // the animation delay (which would restart the animation-in
            // in some browsers)
            if (getStyleName().contains(VOverlay.ADDITIONAL_CLASSNAME_ANIMATE_IN)) {
                AnimationUtil.addAnimationEndListener(getElement(), event -> {
                    if (AnimationUtil.getAnimationName(event).contains(VOverlay.ADDITIONAL_CLASSNAME_ANIMATE_IN)) {
                        VNotification.this.hide();
                    }
                });
            } else {
                VNotification.super.hide();
                fireEvent(new HideEvent(this));
                NOTIFICATIONS.remove(this);
            }
        }
    }

    private void updatePositionOffsets(com.vaadin.shared.Position position) {
        final Element el = getElement();

        // Remove all offsets (GWT PopupPanel defaults)
        el.getStyle().clearTop();
        el.getStyle().clearLeft();

        switch (position) {
        case MIDDLE_LEFT:
        case MIDDLE_RIGHT:
            center();
            el.getStyle().clearLeft();
            break;
        case TOP_CENTER:
        case BOTTOM_CENTER:
            center();
            el.getStyle().clearTop();
            break;
        case MIDDLE_CENTER:
            center();
            break;
        }
    }

    public void setPosition(com.vaadin.shared.Position position) {
        final Element el = getElement();

        // Remove any previous positions
        el.removeClassName(STYLENAME_POSITION_TOP);
        el.removeClassName(STYLENAME_POSITION_RIGHT);
        el.removeClassName(STYLENAME_POSITION_BOTTOM);
        el.removeClassName(STYLENAME_POSITION_LEFT);
        el.removeClassName(STYLENAME_POSITION_MIDDLE);
        el.removeClassName(STYLENAME_POSITION_CENTER);
        el.removeClassName(STYLENAME_POSITION_ASSISTIVE);

        switch (position) {
        case TOP_LEFT:
            el.addClassName(STYLENAME_POSITION_TOP);
            el.addClassName(STYLENAME_POSITION_LEFT);
            break;
        case TOP_RIGHT:
            el.addClassName(STYLENAME_POSITION_TOP);
            el.addClassName(STYLENAME_POSITION_RIGHT);
            break;
        case MIDDLE_LEFT:
            el.addClassName(STYLENAME_POSITION_MIDDLE);
            el.addClassName(STYLENAME_POSITION_LEFT);
            break;
        case MIDDLE_RIGHT:
            el.addClassName(STYLENAME_POSITION_MIDDLE);
            el.addClassName(STYLENAME_POSITION_RIGHT);
            break;
        case BOTTOM_RIGHT:
            el.addClassName(STYLENAME_POSITION_BOTTOM);
            el.addClassName(STYLENAME_POSITION_RIGHT);
            break;
        case BOTTOM_LEFT:
            el.addClassName(STYLENAME_POSITION_BOTTOM);
            el.addClassName(STYLENAME_POSITION_LEFT);
            break;
        case TOP_CENTER:
            el.addClassName(STYLENAME_POSITION_TOP);
            el.addClassName(STYLENAME_POSITION_CENTER);
            break;
        case BOTTOM_CENTER:
            el.addClassName(STYLENAME_POSITION_BOTTOM);
            el.addClassName(STYLENAME_POSITION_CENTER);
            break;
        case ASSISTIVE:
            el.addClassName(STYLENAME_POSITION_ASSISTIVE);
            break;
        }
    }

    @Override
    public void onBrowserEvent(Event event) {
        hide();
    }

    @Override
    /*
     * Fix for #14689: {@link #onEventPreview(Event)} method is deprecated and
     * it's called now only for the very first handler (see super impl). We need
     * it to work for any handler. So let's call old {@link
     * #onEventPreview(Event)} method explicitly with updated logic for {@link
     * #onPreviewNativeEvent(Event)}.
     */
    protected void onPreviewNativeEvent(NativePreviewEvent event) {
        if (!onEventPreview(Event.as(event.getNativeEvent()))) {
            event.cancel();
        }
    }

    @Override
    public boolean onEventPreview(Event event) {
        int type = DOM.eventGetType(event);
        // "modal"
        if (infiniteDelay || temporaryStyle == STYLE_SYSTEM) {
            if (type == Event.ONCLICK || type == Event.ONTOUCHEND) {
                if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
                    hide();
                    return false;
                }
            } else if (type == Event.ONKEYDOWN && event.getKeyCode() == KeyCodes.KEY_ESCAPE) {
                hide();
                return false;
            }
            return temporaryStyle == STYLE_SYSTEM;
        }
        // default
        switch (type) {
        case Event.ONMOUSEMOVE:
            if (x < 0) {
                x = DOM.eventGetClientX(event);
                y = DOM.eventGetClientY(event);
            } else if (Math.abs(DOM.eventGetClientX(event) - x) > MOUSE_MOVE_THRESHOLD
                    || Math.abs(DOM.eventGetClientY(event) - y) > MOUSE_MOVE_THRESHOLD) {
                hideAfterDelay();
            }
            break;
        case Event.ONMOUSEDOWN:
        case Event.ONMOUSEWHEEL:
        case Event.ONSCROLL:
            hideAfterDelay();
            break;
        case Event.ONKEYDOWN:
            if (event.getRepeat()) {
                return true;
            }
            hideAfterDelay();
            break;
        default:
            break;
        }
        return true;
    }

    public void addEventListener(EventListener listener) {
        if (listeners == null) {
            listeners = new ArrayList<>();
        }
        listeners.add(listener);
    }

    public void removeEventListener(EventListener listener) {
        if (listeners != null) {
            listeners.remove(listener);
        }
    }

    private void fireEvent(HideEvent event) {
        if (listeners != null) {
            for (EventListener l : listeners) {
                l.notificationHidden(event);
            }
        }
    }

    /**
     * Creates and shows a {@code Notification} with the specified parameters.
     *
     * @param client
     *            The client connection, cannot be {@code null}.
     * @param caption
     *            The Notification caption, can be {@code null}.
     * @param description
     *            The Notification description, can be {@code null}.
     * @param htmlContentAllowed
     *            Whether {@code caption} and {@code description} are
     *            interpreted as HTML or not.
     * @param iconUri
     *            The icon URI, can be {@code null}.
     * @param styleName
     *            The Notification style name, can be {@code null}.
     * @param position
     *            The desired {@link Position}, can not be {@code null}.
     * @param delayMsec
     *            The delay in milliseconds before disappearing, -1 for forever.
     *
     * @since 8.2
     */
    public static VNotification showNotification(ApplicationConnection client, String caption, String description,
            boolean htmlContentAllowed, String iconUri, String styleName, Position position, int delayMsec) {
        String html = "";
        if (iconUri != null) {
            html += client.getIcon(iconUri).getElement().getString();
        }
        if (caption != null) {
            if (!htmlContentAllowed) {
                caption = WidgetUtil.escapeHTML(caption);
                caption = caption.replaceAll("\\n", "<br />");
            }
            html += "<h1 class='" + getDependentStyle(client, CAPTION) + "'>" + caption + "</h1>";
        }
        if (description != null) {
            if (!htmlContentAllowed) {
                description = WidgetUtil.escapeHTML(description);
                description = description.replaceAll("\\n", "<br />");
            }
            html += "<p class='" + getDependentStyle(client, DESCRIPTION) + "'>" + description + "</p>";
        }

        VNotification vNotification = createNotification(delayMsec, client.getUIConnector().getWidget());

        vNotification.show(html, position, styleName);
        return vNotification;
    }

    /**
     * Meant for internal usage only.
     *
     * @since 7.5.0
     * @param client
     *            application connection
     * @param style
     *            the dependent style name
     * @return the given dependent style name prefixed with current notification
     *         primary style
     */
    public static String getDependentStyle(ApplicationConnection client, String style) {
        VNotification notification = createNotification(-1, client.getUIConnector().getWidget());
        String styleName = notification.getStyleName();
        notification.addStyleDependentName(style);
        String extendedStyle = notification.getStyleName();
        return extendedStyle.substring(styleName.length()).trim();
    }

    public static VNotification createNotification(int delayMsec, Widget owner) {
        final VNotification notification = GWT.create(VNotification.class);
        notification.setWaiAriaRole(null);
        notification.setDelay(delayMsec);

        if (!notification.infiniteDelay && BrowserInfo.get().isTouchDevice()) {
            new Timer() {
                @Override
                public void run() {
                    if (notification.isAttached()) {
                        notification.hide();
                    }
                }
            }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY);
        }
        notification.setOwner(owner);
        return notification;
    }

    public class HideEvent extends EventObject {

        public HideEvent(Object source) {
            super(source);
        }
    }

    public interface EventListener extends java.util.EventListener {
        public void notificationHidden(HideEvent event);
    }

    /**
     * Moves currently visible notifications to the top of the event preview
     * stack. Can be called when opening other overlays such as subwindows to
     * ensure the notifications receive the events they need and don't linger
     * indefinitely. See #7136.
     *
     * TODO Should this be a generic Overlay feature instead?
     */
    public static void bringNotificationsToFront() {
        for (VNotification notification : NOTIFICATIONS) {
            DOM.removeEventPreview(notification);
            DOM.addEventPreview(notification);
        }
    }

    /**
     * Shows an error notification and redirects the user to the given URL when
     * she clicks on the notification.
     *
     * If both message and caption are null, redirects the user to the url
     * immediately
     *
     * @since 7.5.1
     * @param connection
     *            A reference to the ApplicationConnection
     * @param caption
     *            The caption for the error or null to exclude the caption
     * @param message
     *            The message for the error or null to exclude the message
     * @param details
     *            A details message or null to exclude the details
     * @param url
     *            A url to redirect to after the user clicks the error
     *            notification
     */
    public static void showError(ApplicationConnection connection, String caption, String message, String details,
            String url) {

        StringBuilder html = new StringBuilder();
        if (caption != null) {
            html.append("<h1 class='");
            html.append(getDependentStyle(connection, CAPTION));
            html.append("'>");
            html.append(caption);
            html.append("</h1>");
        }
        if (message != null) {
            html.append("<p class='");
            html.append(getDependentStyle(connection, DESCRIPTION));
            html.append("'>");
            html.append(message);
            html.append("</p>");
        }

        if (html.length() != 0) {

            // Add error description
            if (details != null) {
                html.append("<p class='");
                html.append(getDependentStyle(connection, DETAILS));
                html.append("'>");
                html.append("<i style=\"font-size:0.7em\">");
                html.append(details);
                html.append("</i></p>");
            }

            VNotification n = VNotification.createNotification(1000 * 60 * 45,
                    connection.getUIConnector().getWidget());
            n.addEventListener(new NotificationRedirect(url));
            n.show(html.toString(), VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM);
        } else {
            WidgetUtil.redirect(url);
        }
    }

    /**
     * Listens for Notification hide event, and redirects. Used for system
     * messages, such as session expired.
     *
     */
    private static class NotificationRedirect implements VNotification.EventListener {
        String url;

        NotificationRedirect(String url) {
            this.url = url;
        }

        @Override
        public void notificationHidden(HideEvent event) {
            WidgetUtil.redirect(url);
        }

    }

}