From 3367679897a123339f8630498861c726fe0e13f0 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Tue, 10 Mar 2026 12:55:07 -0300 Subject: [PATCH 01/32] feat: implement chat assistant as pure vaadin component --- .../addons/chatassistant/ChatAssistant.java | 666 +++++++++++++----- .../frontend/fc-chat-assistant-movement.js | 101 +++ .../fc-chat-assistant-resize-bottom-left.js | 103 +++ .../fc-chat-assistant-resize-bottom-right.js | 101 +++ .../fc-chat-assistant-resize-bottom.js | 84 +++ .../frontend/fc-chat-assistant-resize-left.js | 85 +++ .../fc-chat-assistant-resize-right.js | 84 +++ .../fc-chat-assistant-resize-top-left.js | 102 +++ .../fc-chat-assistant-resize-top-right.js | 101 +++ .../frontend/fc-chat-assistant-resize-top.js | 84 +++ .../styles/fc-chat-assistant-style.css | 54 ++ .../META-INF/resources/icons/chatbot.svg | 137 ++++ .../chatassistant/ChatAssistantDemo.java | 12 +- .../ChatAssistantGenerativeDemo.java | 6 +- .../ChatAssistantLazyLoadingDemo.java | 5 + .../ChatAssistantMarkdownDemo.java | 4 + .../resources/META-INF/resources/chatbot.svg | 137 ++++ 17 files changed, 1670 insertions(+), 196 deletions(-) create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js create mode 100644 src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css create mode 100644 src/main/resources/META-INF/resources/icons/chatbot.svg create mode 100644 src/test/resources/META-INF/resources/chatbot.svg diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 9c025b0..af00b7a 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -7,9 +7,9 @@ * 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. @@ -17,241 +17,522 @@ * limitations under the License. * #L% */ - package com.flowingcode.vaadin.addons.chatassistant; import com.flowingcode.vaadin.addons.chatassistant.model.Message; -import com.flowingcode.vaadin.jsonmigration.JsonMigration; -import com.vaadin.flow.component.ClickNotifier; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.HasElement; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.dependency.JsModule; -import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.messages.MessageInput; -import com.vaadin.flow.component.messages.MessageInput.SubmitEvent; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.popover.Popover; -import com.vaadin.flow.component.react.ReactAdapterComponent; +import com.vaadin.flow.component.popover.PopoverPosition; import com.vaadin.flow.component.virtuallist.VirtualList; import com.vaadin.flow.data.provider.DataProvider; import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.dom.Style; import com.vaadin.flow.function.SerializableSupplier; import com.vaadin.flow.shared.Registration; + import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import lombok.experimental.ExtensionMethod; -/** - * Component that allows to create a floating chat button that will open a chat window that can be - * used to provide a chat assistant feature. - * - * @author mmlopez - */ -@SuppressWarnings("serial") -@NpmPackage(value = "react-draggable", version = "4.4.6") -@NpmPackage(value = "@mui/material", version = "7.1.2") -@NpmPackage(value = "@mui/icons-material", version = "6.1.0") -@NpmPackage(value = "@emotion/react", version = "11.14.0") -@NpmPackage(value = "@emotion/styled", version = "11.14.0") -@JsModule("./react/animated-fab.tsx") -@JsModule("./fcChatAssistantConnector.js") +@JsModule("./fc-chat-assistant-movement.js") +@JsModule("./fc-chat-assistant-resize-top.js") +@JsModule("./fc-chat-assistant-resize-top-right.js") +@JsModule("./fc-chat-assistant-resize-right.js") +@JsModule("./fc-chat-assistant-resize-bottom-right.js") +@JsModule("./fc-chat-assistant-resize-bottom.js") +@JsModule("./fc-chat-assistant-resize-left.js") +@JsModule("./fc-chat-assistant-resize-bottom-left.js") +@JsModule("./fc-chat-assistant-resize-top-left.js") +@CssImport("./styles/fc-chat-assistant-style.css") @Tag("animated-fab") -@CssImport("./styles/chat-assistant-styles.css") -@ExtensionMethod(value = JsonMigration.class, suppressBaseMethods = true) -public class ChatAssistant extends ReactAdapterComponent implements ClickNotifier> { +public class ChatAssistant extends Div { + + protected SvgIcon fabIcon; + + protected final Button fab = new Button(); + protected final Div unreadBadge = new Div(); + protected final Div fabWrapper = new Div(fab, unreadBadge); + protected final Popover chatWindow = new Popover(); + protected final Div overlay = new Div(); + protected final VerticalLayout container = new VerticalLayout(); - private static final String CHAT_HEADER_CLASS_NAME = "chat-header"; + protected final Div resizerTop = new Div(); + protected final Div resizerBottom = new Div(); + protected final Div resizerTopRight = new Div(); + protected final Div resizerBottomRight = new Div(); + protected final Div resizerRight = new Div(); + protected final Div resizerLeft = new Div(); + protected final Div resizerBottomLeft = new Div(); + protected final Div resizerTopLeft = new Div(); + + protected static final int DEFAULT_FAB_SIZE = 60; + protected static final int DEFAULT_FAB_ICON_SIZE = 45; + protected static final int DEFAULT_FAB_MARGIN = 25; + protected static final int DEFAULT_RESIZER_SIZE = 25; + protected static final int DEFAULT_MAX_RESIZER_SIZE = 150; + protected static final int DEFAULT_DRAG_SENSITIVITY = 10; + + private static final int DEFAULT_CONTENT_MIN_WIDTH = 150; + private static final int DEFAULT_CONTENT_MIN_HEIGHT = 150; + private static final String DEFAULT_POPOVER_TAG = "fc-chat-assistant-popover"; + private static final String DEFAULT_FAB_CLASS = "fc-chat-assistant-fab"; + private static final String DEFAULT_RESIZE_CLASS = "fc-chat-assistant-resize"; + private static final String DEFAULT_UNREAD_BADGE_CLASS = "fc-chat-assistant-unread-badge"; private Component headerComponent; - private VerticalLayout container; private Component footerContainer; - private VirtualList content = new VirtualList<>(); - private Popover chatWindow; + private VirtualList content; private List messages; private MessageInput messageInput; private Span whoIsTyping; - private boolean minimized = true; private Registration defaultSubmitListenerRegistration; - private SerializableSupplier avatarProvider = () -> new Avatar("Chat Assistant"); - private Avatar avatar; + private int unreadMessages; + + public ChatAssistant(List messages, boolean markdownEnabled) { + this.setUI(); + + this.content = new VirtualList(); + this.messages = messages; + this.initializeHeader(); + this.initializeFooter(); + this.initializeContent(markdownEnabled); + this.initializeChatWindow(); + } - /** - * Default constructor. Creates a ChatAssistant with no messages. - */ public ChatAssistant() { - this(new ArrayList<>(), false); + this(new ArrayList(), false); } - /** - * Creates a ChatAssistant with no messages. - * - * @param markdownEnabled flag to enable or disable markdown support - */ public ChatAssistant(boolean markdownEnabled) { - this(new ArrayList<>(), markdownEnabled); + this(new ArrayList(), markdownEnabled); + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + super.onAttach(attachEvent); + addComponentRefreshedListener( + "fc-chat-assistant-drag-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantMovement($0, $1, $2, $3, $4);", + this.getElement(), fabWrapper.getElement(), fab.getElement(), DEFAULT_FAB_MARGIN, DEFAULT_DRAG_SENSITIVITY + ) + ); + chatWindow.addOpenedChangeListener(ev -> { + if(ev.isOpened()) { + addComponentRefreshedListener("fc-chat-assistant-resize-top-listener", this, + () -> + this.getElement().executeJs( + "window.fcChatAssistantResizeTop($0, $1, $2, $3, $4);", + resizerTop.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-bottom-right-listener", this, + () -> + this.getElement().executeJs( + "window.fcChatAssistantResizeBottomRight($0, $1, $2, $3, $4);", + resizerBottomRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-top-right-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeTopRight($0, $1, $2, $3, $4);", + resizerTopRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-right-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeRight($0, $1, $2, $3, $4);", + resizerRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-bottom-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeBottom($0, $1, $2, $3, $4);", + resizerBottom.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-left-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeLeft($0, $1, $2, $3, $4);", + resizerLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-top-left-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeTopLeft($0, $1, $2, $3, $4);", + resizerTopLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + addComponentRefreshedListener( + "fc-chat-assistant-resize-bottom-left-listener", this, + () -> this.getElement().executeJs( + "window.fcChatAssistantResizeBottomLeft($0, $1, $2, $3, $4);", + resizerBottomLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ) + ); + } + }); + } + + private void setUI() { + getStyle() + .setZIndex(1000); + + overlay.getStyle() + .setMinHeight(DEFAULT_CONTENT_MIN_HEIGHT + "px") + .setMinWidth(DEFAULT_CONTENT_MIN_WIDTH + "px"); + + fabIcon = new SvgIcon("/icons/chatbot.svg"); + fabIcon.setSize(DEFAULT_FAB_ICON_SIZE + "px"); + + fab.getStyle() + .setBorderRadius("50%") + .setMinHeight(DEFAULT_FAB_SIZE + "px") + .setMinWidth(DEFAULT_FAB_SIZE + "px") + .setHeight(DEFAULT_FAB_SIZE + "px") + .setWidth(DEFAULT_FAB_SIZE + "px") + .setMaxHeight(DEFAULT_FAB_SIZE + "px") + .setMaxWidth(DEFAULT_FAB_SIZE + "px"); + fab.setIcon(fabIcon); + fab.addClassName(DEFAULT_FAB_CLASS); + fab.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + fabWrapper.getStyle() + .setHeight(DEFAULT_FAB_SIZE + "px") + .setWidth(DEFAULT_FAB_SIZE + "px") + .setDisplay(Style.Display.INLINE_FLEX) + .setAlignItems(Style.AlignItems.CENTER) + .setJustifyContent(Style.JustifyContent.CENTER) + .setPosition(Style.Position.FIXED); + + unreadBadge.setText(String.valueOf(99)); + unreadBadge.addClassName(DEFAULT_UNREAD_BADGE_CLASS); + unreadBadge.getStyle() + .setTextAlign(Style.TextAlign.CENTER) + .setPosition(Style.Position.ABSOLUTE) + .setJustifyContent(Style.JustifyContent.CENTER) + .setAlignItems(Style.AlignItems.CENTER) + .setDisplay(Style.Display.FLEX) + .setPadding("var(--lumo-space-xs)") + .setFontWeight(Style.FontWeight.BOLD) + .setFontSize("var(--lumo-font-size-xs)") + .setBorderRadius("50%") + .setBackgroundColor("var(--lumo-warning-color)") + .setScale("0") + .setMinHeight("var(--lumo-font-size-xs)") + .setMinWidth("var(--lumo-font-size-xs)") + .setHeight("var(--lumo-font-size-xs)") + .setWidth("var(--lumo-font-size-xs)") + .setMaxHeight("var(--lumo-font-size-xs)") + .setMaxWidth("var(--lumo-font-size-xs)") + .setTop("0") + .setRight("0") + .setColor("var(--lumo-warning-text-color)"); + + chatWindow.add(overlay); + chatWindow.setPosition(PopoverPosition.TOP); + chatWindow.addClassName(DEFAULT_POPOVER_TAG); + chatWindow.setOpenOnClick(false); + chatWindow.setTarget(fab); + + applyGenericResizerStyle(resizerTop, "top"); + resizerTop.getStyle() + .setTop("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth("100%"); + + applyGenericResizerStyle(resizerBottom, "bottom"); + resizerBottom.getStyle() + .setBottom("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth("100%"); + + applyGenericResizerStyle(resizerTopRight, "top-right"); + resizerTopRight.getStyle() + .setRight("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + applyGenericResizerStyle(resizerBottomRight, "bottom-right"); + resizerBottomRight.getStyle() + .setBottom("0") + .setRight("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + applyGenericResizerStyle(resizerRight, "right"); + resizerRight.getStyle() + .setRight("0") + .setHeight("100%") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + applyGenericResizerStyle(resizerLeft, "left"); + resizerLeft.getStyle() + .setLeft("0") + .setHeight("100%") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + applyGenericResizerStyle(resizerBottomLeft, "bottom-left"); + resizerBottomLeft.getStyle() + .setBottom("0") + .setLeft("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + applyGenericResizerStyle(resizerTopLeft, "top-left"); + resizerTopLeft.getStyle() + .setTop("0") + .setLeft("0") + .setHeight(DEFAULT_RESIZER_SIZE + "px") + .setWidth(DEFAULT_RESIZER_SIZE + "px"); + + overlay.add( + resizerTop, resizerBottom, + resizerRight, resizerTopRight, resizerBottomRight, + resizerLeft, resizerTopLeft, resizerBottomLeft, + container + ); + add(chatWindow, fabWrapper); + } + + /** Receives click events from the client side to toggle the chat window's opened state. */ + @ClientCallable + protected void onClick() { + if(chatWindow.isOpened()) { + chatWindow.close(); + } + else { + chatWindow.open(); + } } - + + /** Applies common styles to the resizer elements based on the specified direction. */ + protected void applyGenericResizerStyle(Div resizer, String direction) { + resizer.getStyle() + .setPosition(Style.Position.ABSOLUTE) + .setDisplay(Style.Display.INLINE_BLOCK) + .setZIndex(1001); + resizer.addClassName(DEFAULT_RESIZE_CLASS + "-" + direction); + } + /** - * Creates a ChatAssistant with the given list of messages. - * - * @param messages the list of messages - * @param markdownEnabled flag to enable or disable markdown support + * Adds a listener to the given root component that executes the provided + * callback when the DOM instance of the component is replaced (i.e., when the previous + * client-side JavaScript code attached to the old component disappears and a new instance + * is created). + * This is useful for re-initializing client-side logic after component + * refreshes while preventing listeners from stacking. + * + * @param uniqueFlag a unique identifier for the component instance + * @param root the root component to observe + * @param callback the action to execute when the component is refreshed */ - public ChatAssistant(List messages, boolean markdownEnabled) { - this.messages = messages; - initializeHeader(); - initializeFooter(); - initializeContent(markdownEnabled); - initializeChatWindow(); - initializeAvatar(); + protected static void addComponentRefreshedListener(String uniqueFlag, Component root, Runnable callback) { + root.getElement().executeJs( + """ + const flag = $0; + if(document[flag] && document[flag] == this) return true; + document[flag] = this; + return false; + """, uniqueFlag).then(r -> { + if (!r.asBoolean()) + callback.run(); + }); + } + + /** Sets the icon for the floating action button. + *
The icon's size is automatically adjusted to fit within the FAB. */ + public void setFabIcon(Component icon) { + icon.getStyle() + .setWidth(DEFAULT_FAB_ICON_SIZE + "px") + .setHeight(DEFAULT_FAB_ICON_SIZE + "px"); + fab.setIcon(icon); + } + + /** Sets the opened state of the chat window. If true, opens the window; if false, closes it. */ + public void setOpened(boolean opened) { + if(opened) { + chatWindow.open(); + } + else { + chatWindow.close(); + } + } + + /** Opens the chat window. */ + public void open() { + chatWindow.open(); + } + + /** Closes the chat window. */ + public void close() { + chatWindow.close(); + } + + /** Sets the chat window minimum width. Applies when resizing. **/ + public void setWindowMinWidth(String minWidth) { + this.overlay.setMinWidth(minWidth); + } + + /** Sets the chat window minimum height. Applies when resizing. **/ + public void setWindowMinHeight(String minHeight) { + this.overlay.setMinHeight(minHeight); + } + + /** Sets the chat window maximum width. Applies when resizing. **/ + public void setWindowMaxWidth(String maxWidth) { + this.overlay.setMaxWidth(maxWidth); + } + + /** Sets the chat window maximum height. Applies when resizing. **/ + public void setWindowMaxHeight(String maxHeight) { + this.overlay.setMaxHeight(maxHeight); + } + + /** Sets the chat window default height. Applies when resizing. **/ + public void setWindowHeight(String height) { + this.overlay.setHeight(height); + } + + /** Sets the chat window default width. Applies when resizing. **/ + public void setWindowWidth(String width) { + this.overlay.setWidth(width); } + @SuppressWarnings("unchecked") private void initializeHeader() { Icon minimize = VaadinIcon.CLOSE.create(); - minimize.addClickListener(ev -> setMinimized(!minimized)); + minimize.addClickListener((ev) -> onClick()); Span title = new Span("Chat Assistant"); title.setWidthFull(); - HorizontalLayout header = new HorizontalLayout(title, minimize); + HorizontalLayout header = new HorizontalLayout(new Component[]{title, minimize}); header.setWidthFull(); - headerComponent = header; + this.headerComponent = header; } @SuppressWarnings("unchecked") private void initializeFooter() { - messageInput = new MessageInput(); - messageInput.setWidthFull(); - messageInput.setMaxHeight("80px"); - messageInput.getStyle().set("padding", "0"); - defaultSubmitListenerRegistration = messageInput.addSubmitListener(se -> { - sendMessage((T) Message.builder().messageTime(LocalDateTime.now()) - .name("User").content(se.getValue()).build()); - }); - whoIsTyping = new Span(); - whoIsTyping.setClassName("chat-assistant-who-is-typing"); - whoIsTyping.setVisible(false); - VerticalLayout footer = new VerticalLayout(whoIsTyping, messageInput); + this.messageInput = new MessageInput(); + this.messageInput.setWidthFull(); + this.messageInput.setMaxHeight("80px"); + this.messageInput.getStyle().set("padding", "0"); + this.defaultSubmitListenerRegistration = this.messageInput.addSubmitListener((se) -> this.sendMessage( + (T) Message.builder().messageTime( + LocalDateTime.now()).name("User").content(se.getValue()).build())); + this.whoIsTyping = new Span(); + this.whoIsTyping.setClassName("chat-assistant-who-is-typing"); + this.whoIsTyping.setVisible(false); + VerticalLayout footer = new VerticalLayout(new Component[]{this.whoIsTyping, this.messageInput}); footer.setWidthFull(); footer.setSpacing(false); footer.setMargin(false); footer.setPadding(false); - footerContainer = footer; + this.footerContainer = footer; } @SuppressWarnings("unchecked") private void initializeContent(boolean markdownEnabled) { - content.setRenderer(new ComponentRenderer<>(message -> new ChatMessage(message, markdownEnabled), - (component, message) -> { - ((ChatMessage) component).setMessage(message); - return component; - })); - content.setItems(messages); - content.setSizeFull(); - container = new VerticalLayout(headerComponent, content, footerContainer); - container.setClassName("chat-assistant-container-vertical-layout"); - container.setPadding(false); - container.setMargin(false); - container.setSpacing(false); - container.setSizeFull(); - container.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - container.setFlexGrow(1, content); + this.content.setRenderer(new ComponentRenderer((message) -> new ChatMessage((Message) message, markdownEnabled), (component, message) -> { + ((ChatMessage)component).setMessage((Message) message); + return component; + })); + this.content.setItems(this.messages); + this.content.setSizeFull(); + this.container.add(this.headerComponent, this.content, this.footerContainer); + this.container.setPadding(true); + this.container.setMargin(false); + this.container.setSpacing(false); + this.container.setSizeFull(); + this.container.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); + this.container.setFlexGrow((double)1.0F, new HasElement[]{this.content}); } private void initializeChatWindow() { - VerticalLayout resizableVL = new VerticalLayout(); - resizableVL.setClassName("chat-assistant-resizable-vertical-layout"); - resizableVL.add(container); - chatWindow = new Popover(); - chatWindow.add(resizableVL); - chatWindow.setOpenOnClick(false); - chatWindow.setCloseOnOutsideClick(false); - chatWindow.addOpenedChangeListener(ev -> minimized = !ev.isOpened()); - chatWindow.addAttachListener(e -> e.getUI().getPage() - .executeJs("window.Vaadin.Flow.fcChatAssistantConnector.observePopoverResize($0)", chatWindow.getElement())); - - this.getElement().addEventListener("avatar-clicked", ev ->{ - if (this.minimized) { - chatWindow.open(); - } else { - chatWindow.close(); - } - }); - } - - private void initializeAvatar() { - if (avatar!=null) { - avatar.removeFromParent(); - } - avatar = avatarProvider.get(); - this.getElement().appendChild(avatar.getElement()); - this.addAttachListener(ev -> this.getElement().executeJs("return;") - .then(ev2 -> this.getElement().executeJs("this.childNodes[1].childNodes[0].childNodes[0].appendChild($0)", avatar.getElement()) - .then(ev3 -> { - chatWindow.setTarget(avatar); - avatar.setSizeFull(); - }))); + this.chatWindow.setOpenOnClick(false); + this.chatWindow.setCloseOnOutsideClick(false); } - /** - * Sets the data provider of the internal VirtualList. - * - * @param dataProvider the data provider to be used - */ + @SuppressWarnings("unchecked") public void setDataProvider(DataProvider dataProvider) { - content.setDataProvider(dataProvider); + this.content.setDataProvider(dataProvider); } /** * Uses the provided string as the text shown over the message input to indicate that someone is typing. - * + * * @param whoIsTyping string to be shown as an indication of someone typing */ public void setWhoIsTyping(String whoIsTyping) { this.whoIsTyping.setText(whoIsTyping); this.whoIsTyping.setVisible(true); } - + /** * Returns the current text shown over the message input to indicate that someone is typing. - * + * * @return the current text or null if not configured */ public String getWhoIsTyping() { return whoIsTyping.getText(); } - + /** * Clears the text shown over the message input to indicate that someone is typing. */ public void clearWhoIsTyping() { - this.whoIsTyping.setText(null); + this.whoIsTyping.setText((String)null); this.whoIsTyping.setVisible(false); } - + /** * Sets the SubmitListener that will be notified when the user submits a message on the underlying messageInput. - * + * * @param listener the listener that will be notified when the SubmitEvent is fired * @return registration for removal of the listener */ - public Registration setSubmitListener(ComponentEventListener listener) { - defaultSubmitListenerRegistration.remove(); - return messageInput.addSubmitListener(listener); + public Registration setSubmitListener(ComponentEventListener listener) { + this.defaultSubmitListenerRegistration.remove(); + return this.messageInput.addSubmitListener(listener); } private void refreshContent() { - content.getDataProvider().refreshAll(); - content.scrollToEnd(); + this.content.getDataProvider().refreshAll(); + this.content.scrollToEnd(); } /** @@ -261,14 +542,14 @@ private void refreshContent() { * @param message the message to be sent programmatically */ public void sendMessage(T message) { - messages.add(message); - content.getDataProvider().refreshAll(); - content.scrollToEnd(); + this.messages.add(message); + this.content.getDataProvider().refreshAll(); + this.content.scrollToEnd(); } - + /** * Updates a previously entered message. - * + * * @param message the message to be updated */ public void updateMessage(T message) { @@ -277,76 +558,71 @@ public void updateMessage(T message) { /** * Shows or hides chat window. - * + * * @param minimized true for hiding the chat window and false for displaying it */ public void setMinimized(boolean minimized) { - if (this.minimized != minimized) { - this.minimized = minimized; - if (!minimized) { - refreshContent(); - } - } - if (minimized && chatWindow.isOpened()) { - chatWindow.close(); - } else if (!minimized && !chatWindow.isOpened()) { - chatWindow.open(); + if (minimized && this.chatWindow.isOpened()) { + this.chatWindow.close(); + } else if (!minimized && !this.chatWindow.isOpened()) { + this.chatWindow.open(); } } - + /** * Returns the visibility of the chat window. - * + * * @return true if the chat window is minimized false otherwise */ public boolean isMinimized() { - return minimized; + return !chatWindow.isOpened(); } - + /** * Allows changing the header of the chat window. - * + * * @param component to be used as a replacement for the header */ public void setHeaderComponent(Component component) { - if (headerComponent != null) { - container.remove(headerComponent); + if (this.headerComponent != null) { + this.container.remove(new Component[]{this.headerComponent}); } - component.addClassName(CHAT_HEADER_CLASS_NAME); - headerComponent = component; - container.addComponentAsFirst(headerComponent); + + component.addClassName("chat-header"); + this.headerComponent = component; + this.container.addComponentAsFirst(this.headerComponent); } - + /** * Returns the current component configured as the header of the chat window. - * + * * @return component used as the header of the chat window */ public Component getHeaderComponent() { - return headerComponent; + return this.headerComponent; } - + /** * Allows changing the footer of the chat window. - * + * * @param component to be used as a replacement for the footer, it cannot be null */ public void setFooterComponent(Component component) { Objects.requireNonNull(component, "Component cannot not be null"); - container.remove(footerContainer); - footerContainer = component; - container.add(footerContainer); + this.container.remove(new Component[]{this.footerContainer}); + this.footerContainer = component; + this.container.add(new Component[]{this.footerContainer}); } - + /** * Returns the current component configured as the footer of the chat window. - * - * @return component used as the footer of the chat window + * + * @return component used as the footer of the chat window */ public Component getFooterComponent() { - return footerContainer; + return this.footerContainer; } - + /** * Scrolls to the given position. Scrolls so that the element is shown at * the start of the visible area whenever possible. @@ -360,21 +636,21 @@ public Component getFooterComponent() { public void scrollToIndex(int position) { this.content.scrollToIndex(position); } - + /** * Scrolls to the first element. */ public void scrollToStart() { this.content.scrollToStart(); } - + /** * Scrolls to the last element of the list. */ public void scrollToEnd() { this.content.scrollToEnd(); } - + /** * Allows changing the renderer used to display messages in the chat window. * @@ -382,26 +658,28 @@ public void scrollToEnd() { */ public void setMessagesRenderer(Renderer renderer) { Objects.requireNonNull(renderer, "Renderer cannot not be null"); - content.setRenderer(renderer); + this.content.setRenderer(renderer); } - + /** * Sets the avatar provider that will be used to create the avatar - * + * * @param avatarProvider + * @deprecated use {@link #setFabIcon(Component)} instead */ + @Deprecated(since = "5.0.0", forRemoval = true) public void setAvatarProvider(SerializableSupplier avatarProvider) { - this.avatarProvider = avatarProvider; - this.initializeAvatar(); + Objects.requireNonNull(avatarProvider, "Avatar provider cannot be null"); + Avatar avatar = avatarProvider.get(); + setFabIcon(avatar); } - /** - * Return the number of unread messages to be displayed in the chat assistant. - * @return the number of unread messages - */ + /** + * Return the number of unread messages to be displayed in the chat assistant. + * @return the number of unread messages + */ public int getUnreadMessages() { - Integer unreadMessages = getState("unreadMessages", Integer.class); - return unreadMessages==null?0:unreadMessages; + return Math.max(unreadMessages, 0); } /** @@ -409,7 +687,13 @@ public int getUnreadMessages() { * @param unreadMessages */ public void setUnreadMessages(int unreadMessages) { - setState("unreadMessages",unreadMessages); + this.unreadMessages = unreadMessages >= 0 ? Math.min(unreadMessages, 99) : 0; + unreadBadge.setText(String.valueOf(unreadMessages)); + if(unreadMessages > 0) { + unreadBadge.getStyle().setScale("1"); + } + else { + unreadBadge.getStyle().setScale("0"); + } } - } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js new file mode 100644 index 0000000..39c70ae --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -0,0 +1,101 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantMovement = (root, item, fab, marginRaw, sensitivityRaw) => { + const itemWidth = fab.clientWidth; + const itemHeight = fab.clientHeight; + const margin = parseFloat(marginRaw); + const sensitivity = parseFloat(sensitivityRaw); + const sizeTransition = 'transform 0.2s ease'; + const snapTransition = 'all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; + const position = { x: margin, y: margin }; + const initialPosition = { x: margin, y: margin }; + + let screenWidth = window.innerWidth; + let screenHeight = window.innerHeight; + let isDragging = false; + + item.style.transition = sizeTransition; + + window.addEventListener("resize", (_) => { + screenWidth = window.innerWidth; + screenHeight = window.innerHeight; + // Reposition the item to ensure it stays within the new screen bounds + position.x = margin; + position.y = margin; + updatePosition(); + }); + + // Update FAB position + function updatePosition() { + item.style.right = position.x + 'px'; + item.style.bottom = position.y + 'px'; + } + + // Ensure the item stays within the screen and margin bounds + function snapToBoundary() { + const xMax = screenWidth - itemWidth - margin; + const yMax = screenHeight - itemHeight - margin; + const x = position.x; + const y = position.y; + if (x < margin) position.x = margin; + if (x > xMax) position.x = xMax; + if (y < margin) position.y = margin; + if (y > yMax) position.y = yMax; + updatePosition(); + } + + // Determine if the pointer event should be treated as a click (no significant movement, based on sensitivity threshold) + function isClickOnlyEvent() { + const dx = Math.abs(position.x - initialPosition.x); + const dy = Math.abs(position.y - initialPosition.y); + return dx < sensitivity && dy < sensitivity; + } + + item.addEventListener('pointerdown', (e) => { + isDragging = true; + fab.classList.add('dragging'); + item.setPointerCapture(e.pointerId); + item.style.transition = sizeTransition; + initialPosition.x = position.x; + initialPosition.y = position.y; + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetX = screenWidth - e.clientX - itemWidth / 2; + const offsetY = screenHeight - e.clientY - itemHeight / 2; + position.x = offsetX; + position.y = offsetY; + updatePosition(); + }); + + item.addEventListener('pointerup', (e) => { + isDragging = false; + item.style.transition = snapTransition + ', ' + sizeTransition; + fab.classList.remove('dragging'); + item.releasePointerCapture(e.pointerId); + snapToBoundary(); + if (isClickOnlyEvent()) { + root.$server.onClick(); + } + }); + + updatePosition(); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js new file mode 100644 index 0000000..f3c439e --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -0,0 +1,103 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + const overlayArrowCenteredAttribute = "arrow-centered"; + + let minWidth = 0; + let minHeight = 0; + let maxWidth = Infinity; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding directions + function shouldDrag() { + const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; + const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + return bottomRule && leftRule; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + newHeight = offsetY + container.clientHeight; + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginLeft = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginLeft = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js new file mode 100644 index 0000000..ab511f5 --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js @@ -0,0 +1,101 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minWidth = 0; + let minHeight = 0; + let maxWidth = Infinity; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding directions + function shouldDrag() { + const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; + const rightRule = overlay && overlay.style.left && !overlay.style.right; + return bottomRule && rightRule; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginRight = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginRight = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js new file mode 100644 index 0000000..549059e --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js @@ -0,0 +1,84 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minHeight = 0; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding direction + function shouldDrag() { + return overlay && !overlay.style.bottom && overlay.style.top; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.marginBottom = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.marginBottom = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js new file mode 100644 index 0000000..05a74c5 --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -0,0 +1,85 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + const overlayArrowCenteredAttribute = "arrow-centered"; + + let minWidth = 0; + let maxWidth = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding direction + function shouldDrag() { + return overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + } + + item.addEventListener('pointerenter', (_) => { + if (shouldDrag()) { + item.classList.add('active'); + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.width = maxSize + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.width = size + 'px'; + item.style.marginLeft = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.width = size + 'px'; + item.style.marginLeft = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js new file mode 100644 index 0000000..341424c --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js @@ -0,0 +1,84 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minWidth = 0; + let maxWidth = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding direction + function shouldDrag() { + return overlay && overlay.style.left && !overlay.style.right; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.width = maxSize + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.width = size + 'px'; + item.style.marginRight = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.width = size + 'px'; + item.style.marginRight = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js new file mode 100644 index 0000000..5ba9059 --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -0,0 +1,102 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + const overlayArrowCenteredAttribute = "arrow-centered"; + + let minWidth = 0; + let minHeight = 0; + let maxWidth = Infinity; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding directions + function shouldDrag() { + const topRule = overlay && overlay.style.bottom && !overlay.style.top; + const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + return topRule && leftRule; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginLeft = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginLeft = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js new file mode 100644 index 0000000..f410353 --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js @@ -0,0 +1,101 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minWidth = 0; + let minHeight = 0; + let maxWidth = Infinity; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding directions + function shouldDrag() { + const topRule = overlay && overlay.style.bottom && !overlay.style.top; + const rightRule = overlay && overlay.style.left && !overlay.style.right; + return topRule && rightRule; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginRight = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginRight = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js new file mode 100644 index 0000000..d60145b --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js @@ -0,0 +1,84 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ +window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minHeight = 0; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + + // Restrict dragging capability to when the popover content has enough space in the corresponding direction + function shouldDrag() { + return overlay && overlay.style.bottom && !overlay.style.top; + } + + item.addEventListener('pointerenter', (e) => { + if (shouldDrag()) { + item.classList.add('active'); + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (_) => { + isDragging = shouldDrag(); + if (isDragging) { + item.style.height = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + }); + + item.addEventListener('pointerup', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.marginTop = ''; + }); + + item.addEventListener('pointerleave', (_) => { + isDragging = false; + item.style.height = size + 'px'; + item.style.marginTop = ''; + }); +}; diff --git a/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css new file mode 100644 index 0000000..e3d3208 --- /dev/null +++ b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css @@ -0,0 +1,54 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ + +/* Base styles */ +.fc-chat-assistant-fab { + cursor: pointer; + transform: scale(1); + box-shadow: var(--lumo-box-shadow-m); +} + +/* Expansion state when being dragged */ +.fc-chat-assistant-fab.dragging { + cursor: grabbing; + transform: scale(1.15); + box-shadow: var(--lumo-box-shadow-xl); +} + +.fc-chat-assistant-popover { + --vaadin-popover-offset-top : 15px; + --vaadin-popover-offset-bottom : 15px; +} + +.fc-chat-assistant-unread-badge { + transition: all 0.15s ease-out; +} + +/* Specific cursors for each corner */ +.fc-chat-assistant-resize-bottom-left.active { cursor: sw-resize; } /* North-West */ +.fc-chat-assistant-resize-bottom-right.active { cursor: se-resize; } /* North-East */ +.fc-chat-assistant-resize-top-left.active { cursor: nw-resize; } /* South-West */ +.fc-chat-assistant-resize-top-right.active { cursor: ne-resize; } /* South-East */ + +/* Specific cursors for the sides */ +.fc-chat-assistant-resize-bottom.active { cursor: s-resize; } /* North/Top */ +.fc-chat-assistant-resize-top.active { cursor: n-resize; } /* South/Bottom */ +.fc-chat-assistant-resize-left.active { cursor: w-resize; } /* West/Left */ +.fc-chat-assistant-resize-right.active { cursor: e-resize; } /* East/Right */ diff --git a/src/main/resources/META-INF/resources/icons/chatbot.svg b/src/main/resources/META-INF/resources/icons/chatbot.svg new file mode 100644 index 0000000..b8a0851 --- /dev/null +++ b/src/main/resources/META-INF/resources/icons/chatbot.svg @@ -0,0 +1,137 @@ + + + + + Chatbot icon + + + + + + + + + + + + + image/svg+xml + + Chatbot icon + 2018-09-30 + + + m1981 + + + + + Public domain + + + + + Inkscape + + + + + icon + flat + IT + computer + chatbot + question + deeplearning + AI + artificial + intelligence + chat + bot + + + + An AI chatbot icon. + + + + + + + diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java index 5591522..b1fa3e6 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java @@ -26,6 +26,8 @@ import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.data.renderer.ComponentRenderer; @@ -43,7 +45,9 @@ public class ChatAssistantDemo extends VerticalLayout { public ChatAssistantDemo() { ChatAssistant chatAssistant = new ChatAssistant<>(); - chatAssistant.setAvatarProvider(()->new Avatar("Chat Assistant","chatbot.png")); + chatAssistant.setFabIcon(new SvgIcon("chatbot.svg")); + chatAssistant.setWindowWidth("400px"); + chatAssistant.setWindowHeight("400px"); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant"); message.setSizeFull(); @@ -54,7 +58,7 @@ public ChatAssistantDemo() { }); message.addBlurListener(ev->chatAssistant.clearWhoIsTyping()); chatAssistant.setMessagesRenderer(new ComponentRenderer(m -> { - return new CustomChatMessage(m); + return new CustomChatMessage(m); }, (component, m) -> { ((CustomChatMessage) component).setMessage(m); @@ -97,9 +101,9 @@ public void run() { chatAssistant.sendMessage(CustomMessage.builder().content("Hello, I am here to assist you") .messageTime(LocalDateTime.now()) .name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build()); - + chatAssistant.setUnreadMessages(4); - add(message, chat, chatWithThinking, chatAssistant); + add(chatAssistant); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java index aa18f3e..37352dc 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java @@ -26,6 +26,8 @@ import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.data.renderer.ComponentRenderer; @@ -52,7 +54,9 @@ public ChatAssistantGenerativeDemo() { + " the next word. I hope this is useful for your demonstration!"; ChatAssistant chatAssistant = new ChatAssistant<>(); - chatAssistant.setAvatarProvider(()->new Avatar("Chat Assistant","chatbot.png")); + chatAssistant.setFabIcon(new SvgIcon("chatbot.svg")); + chatAssistant.setWindowWidth("400px"); + chatAssistant.setWindowHeight("400px"); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant"); message.setSizeFull(); diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java index 30fe247..961bdb2 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java @@ -27,6 +27,7 @@ import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; @@ -125,6 +126,9 @@ public class ChatAssistantLazyLoadingDemo extends VerticalLayout { public ChatAssistantLazyLoadingDemo() { ChatAssistant chatAssistant = new ChatAssistant<>(); chatAssistant.setClassName("small"); + chatAssistant.setFabIcon(new SvgIcon("chatbot.svg")); + chatAssistant.setWindowWidth("400px"); + chatAssistant.setWindowHeight("400px"); Span lazyLoadingData = new Span(); DataProvider dataProvider = DataProvider.fromCallbacks(query->{ lazyLoadingData.setText("Loading messages from: " + query.getOffset() + ", with limit: " + query.getLimit()); @@ -133,6 +137,7 @@ public ChatAssistantLazyLoadingDemo() { return messages.size(); }); chatAssistant.setDataProvider(dataProvider); + chatAssistant.setFabIcon(new SvgIcon("chatbot.svg")); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant"); diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java index 59e9c06..6160e19 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java @@ -25,6 +25,7 @@ import com.google.common.base.Strings; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.router.PageTitle; @@ -40,6 +41,9 @@ public class ChatAssistantMarkdownDemo extends VerticalLayout { public ChatAssistantMarkdownDemo() { ChatAssistant chatAssistant = new ChatAssistant<>(true); + chatAssistant.setFabIcon(new SvgIcon("chatbot.svg")); + chatAssistant.setWindowWidth("400px"); + chatAssistant.setWindowHeight("400px"); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant (try using Markdown)"); message.setSizeFull(); diff --git a/src/test/resources/META-INF/resources/chatbot.svg b/src/test/resources/META-INF/resources/chatbot.svg new file mode 100644 index 0000000..b8a0851 --- /dev/null +++ b/src/test/resources/META-INF/resources/chatbot.svg @@ -0,0 +1,137 @@ + + + + + Chatbot icon + + + + + + + + + + + + + image/svg+xml + + Chatbot icon + 2018-09-30 + + + m1981 + + + + + Public domain + + + + + Inkscape + + + + + icon + flat + IT + computer + chatbot + question + deeplearning + AI + artificial + intelligence + chat + bot + + + + An AI chatbot icon. + + + + + + + From b58d1b62daec6a0bbe1df12b9aa64d6d400c2027 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Tue, 10 Mar 2026 12:55:25 -0300 Subject: [PATCH 02/32] refactor: remove unused files --- .../META-INF/frontend/react/animated-fab.tsx | 88 --------- .../frontend/styles/chat-assistant-styles.css | 75 -------- .../frontend/fcChatAssistantConnector.js | 178 ------------------ 3 files changed, 341 deletions(-) delete mode 100644 src/main/resources/META-INF/frontend/react/animated-fab.tsx delete mode 100644 src/main/resources/META-INF/frontend/styles/chat-assistant-styles.css delete mode 100644 src/main/resources/META-INF/resources/frontend/fcChatAssistantConnector.js diff --git a/src/main/resources/META-INF/frontend/react/animated-fab.tsx b/src/main/resources/META-INF/frontend/react/animated-fab.tsx deleted file mode 100644 index 4941d6e..0000000 --- a/src/main/resources/META-INF/frontend/react/animated-fab.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import {useState} from 'react'; -import Draggable from 'react-draggable'; -import Fab from '@mui/material/Fab'; -import Badge from '@mui/material/Badge'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; -import { ReactAdapterElement, type RenderHooks } from 'Frontend/generated/flow/ReactAdapter'; - -const animatedFabTheme = createTheme({ - palette: { - primary: { - main: 'var(--lumo-primary-color)', - light: 'var(--lumo-primary-color-50pct)', - dark: 'var(--lumo-primary-color-20pct)', - contrastText: 'rgb(var(--lumo-primary-contrast-color))', - }, - warning: { - main: 'var(--lumo-warning-color, var(--aura-yellow))', - light: 'var(--lumo-warning-color-50pct)', - dark: 'var(--lumo-warning-color-20pct)', - contrastText: 'rgb(var(--lumo-warning-contrast-color, var(--aura-accent-color-light)))', - } - }, - components: { - MuiFab: { - styleOverrides: { - root: ({ theme }) => ({ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - '&:hover': { - backgroundColor: 'var(--lumo-primary-color-50pct)', - }, - }), - }, - }, - } - }); - - -class AnimatedFABElement extends ReactAdapterElement { - private draggableNodeRef = React.createRef(); - - protected override render(hooks: RenderHooks): ReactElement | null { - const [isDragging, setIsDragging] = useState(false); - const [unreadMessages] = hooks.useState('unreadMessages'); - const eventControl = (event: { type: any; }) => { - if (event.type === 'mousemove' || event.type === 'touchmove') { - setIsDragging(true) - } - if (event.type === 'mouseup' || event.type === 'touchend') { - setTimeout(() => { - setIsDragging(false); - }, 100); - } - } - return ( - - -
{if (!isDragging) {this.dispatchEvent(new CustomEvent('avatar-clicked'));}}} - onTouchEndCapture={(event) => {if (!isDragging) {this.dispatchEvent(new CustomEvent('avatar-clicked'));}}} - ref={this.draggableNodeRef} - style={{ - position: 'fixed', - bottom: 16, - right: 16 - }} - > - - - - -
-
-
- ); - } -} - -customElements.define('animated-fab', AnimatedFABElement); diff --git a/src/main/resources/META-INF/frontend/styles/chat-assistant-styles.css b/src/main/resources/META-INF/frontend/styles/chat-assistant-styles.css deleted file mode 100644 index 13737f8..0000000 --- a/src/main/resources/META-INF/frontend/styles/chat-assistant-styles.css +++ /dev/null @@ -1,75 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -vaadin-vertical-layout.chat-assistant-resizable-vertical-layout { - transform: rotate(180deg); /* This rotation, along with the one below, is a "double rotation trick." */ - margin: 0; - padding: 10px; - overflow: hidden; - resize: both; - min-height: 30vh; - min-width: 310px; - width: var(--fc-chat-assistant-popover-width, 400px); - height: var(--fc-chat-assistant-popover-height, 400px); -} - -vaadin-vertical-layout.chat-assistant-container-vertical-layout { - flex-grow: 1; - gap: var(--lumo-space-s, var(--vaadin-gap-s)); - transform: rotate(180deg); /* This second rotation completes the "double rotation trick." - Together, these two rotations position the resize handle in the upper-left corner. - This new position is more suitable for resizing the chat window because the chat bubble - is positioned by default in the bottom-right of the view. */ -} - -.MuiBadge-badge { - z-index: 2000 !important; -} - -vaadin-popover-overlay::part(content){ - padding: 0; -} - -vaadin-message::part(message) { - word-break: break-word; -} - -vaadin-popover-overlay::part(overlay) { - max-width: 100vw; /* Prevent width beyond viewport */ - max-height: 100vh; /* Prevent height beyond viewport */ -} - -/* Mobile breakpoint */ -@media (max-width: 768px) { - vaadin-popover-overlay::part(overlay) { - width: 100%; - height: 100%; - } - - vaadin-popover-overlay::part(content) { - width: 100%; - height: 100%; - } - - vaadin-vertical-layout.chat-assistant-resizable-vertical-layout { - resize: none; - height: 100%; - width: 100%; - } -} \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/frontend/fcChatAssistantConnector.js b/src/main/resources/META-INF/resources/frontend/fcChatAssistantConnector.js deleted file mode 100644 index 7f4b888..0000000 --- a/src/main/resources/META-INF/resources/frontend/fcChatAssistantConnector.js +++ /dev/null @@ -1,178 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2025 Flowing Code - * %% - * 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. - * #L% - */ -(function () { - window.Vaadin.Flow.fcChatAssistantConnector = { - observePopoverResize: (popover) => { - // Skip the following logic on mobile devices by checking viewport width. - if (window.innerWidth <= 768) { - return; - } - - if (popover.$connector) { - return; - } - - popover.$connector = {}; - - // Find the resizable container inside the popover - const resizableContainer = popover.querySelector('.chat-assistant-resizable-vertical-layout'); - if (!resizableContainer) return; - - popover.addEventListener('opened-changed', e => { - if (e.detail.value) { - const popoverOverlay = resizableContainer.parentElement; - const overlay = popoverOverlay.shadowRoot?.querySelector('[part="overlay"]'); - // Track overlay position changes and keep container inside viewport - trackOverlayPosition(overlay, resizableContainer, () => clampToViewport(resizableContainer)); - } - }); - - // On drag/resize start (mouse), reset size restrictions so user can freely resize - resizableContainer.addEventListener("mousedown", e => { - resizableContainer.style.maxHeight = ''; - resizableContainer.style.maxWidth = ''; - }); - // On drag/resize start (touch), reset size restrictions so user can freely resize - resizableContainer.addEventListener("touchstart", e => { - resizableContainer.style.maxHeight = ''; - resizableContainer.style.maxWidth = ''; - }); - - // Debounce calls to avoid excessive recalculations on rapid resize - const debouncedClamp = debounce(() => clampToViewport(resizableContainer)); - - new ResizeObserver(() => { - const popoverOverlay = resizableContainer.parentElement; - const overlay = popoverOverlay.shadowRoot?.querySelector('[part="overlay"]'); - if (!overlay) return; - - debouncedClamp(); - }).observe(resizableContainer); - - - function debounce(callback) { - let rafId; - return () => { - cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(callback); - }; - } - - /** - * Restricts the size and position of a resizable container so that it remains fully visible - * within the browser's viewport, applying a small padding to keep it from touching the edges. - * - * This function calculates how much space is available on each side of the container - * (top, bottom, left, right) relative to the viewport. If the container would overflow - * on a given side, it adjusts `maxWidth`/`maxHeight` and aligns it to the opposite side - * with a fixed padding. - * - * - If there isn't enough space on the right, it clamps width and aligns to the left. - * - If there isn't enough space on the left, it clamps width and aligns to the right. - * - If there isn't enough space at the bottom, it clamps height and aligns to the top. - * - If there isn't enough space at the top, it clamps height and aligns to the bottom. - * - * @param {HTMLElement} resizableContainer - The element whose size and position should be clamped to the viewport. - */ - function clampToViewport(resizableContainer) { - const boundingClientRect = resizableContainer.getBoundingClientRect(); - - const containerWidthRight = boundingClientRect.width + (window.innerWidth - boundingClientRect.right); - const containerWidthLeft = boundingClientRect.left + boundingClientRect.width; - const containerHeightBottom = boundingClientRect.height + (window.innerHeight - boundingClientRect.bottom); - const containerHeightTop = boundingClientRect.top + boundingClientRect.height; - - const padding = 5; - const paddingPx = padding + "px"; - - if (containerWidthRight >= window.innerWidth) { - resizableContainer.style.maxWidth = (boundingClientRect.right - padding) + "px"; - resizableContainer.style.left = paddingPx; - } else if (containerWidthLeft >= window.innerWidth) { - resizableContainer.style.maxWidth = (window.innerWidth - boundingClientRect.left - padding) + "px"; - resizableContainer.style.right = paddingPx; - } - - if (containerHeightBottom >= window.innerHeight) { - resizableContainer.style.maxHeight = (boundingClientRect.bottom - padding) + "px"; - resizableContainer.style.top = paddingPx; - } else if (containerHeightTop >= window.innerHeight) { - resizableContainer.style.maxHeight = (window.innerHeight - boundingClientRect.top - padding) + "px"; - resizableContainer.style.bottom = paddingPx; - } - } - - /** - * Continuously tracks the position of an overlay element and triggers a callback - * when the overlay's position has stabilized (i.e., changes are within the given buffer). - * - * This function uses `requestAnimationFrame` to check the overlay's position every frame. - * If the overlay moves more than `positionBuffer` pixels horizontally or vertically, - * tracking continues without calling the callback. - * Once the position changes are smaller than `positionBuffer`, the callback is invoked. - * - * @param {HTMLElement} overlay - The overlay element to track. Must support `.checkVisibility()`. - * @param {HTMLElement} resizableContainer - The container related to the overlay (not used directly here, - * but often used by the callback to adjust size). - * @param {Function} callback - Function to call when the overlay position is stable. - * @param {number} [positionBuffer=10] - The minimum pixel movement threshold before considering the overlay stable. - */ - function trackOverlayPosition(overlay, resizableContainer, callback, positionBuffer = 10) { - let lastTop = 0; - let lastLeft = 0; - let frameId; - - function checkPosition() { - if (!isVisible(overlay)) { - cancelAnimationFrame(frameId); - return; - } - - const rect = overlay.getBoundingClientRect(); - const deltaTop = Math.abs(rect.top - lastTop); - const deltaLeft = Math.abs(rect.left - lastLeft); - if (deltaTop > positionBuffer || deltaLeft > positionBuffer) { - lastTop = rect.top; - lastLeft = rect.left; - } else { - callback(); - } - - frameId = requestAnimationFrame(checkPosition); - } - - frameId = requestAnimationFrame(checkPosition); - } - - function isVisible(el) { - if (!el) return false; - - if (typeof el.checkVisibility === 'function') { - // Use native checkVisibility if available - return el.checkVisibility(); - } - - // Fallback: check CSS display and visibility - const style = getComputedStyle(el); - return style.display !== 'none' && style.visibility !== 'hidden'; - } - }, - } -})(); From 4b17c35e17934f64ae69ed94be0b0e6327f67b96 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Tue, 10 Mar 2026 12:56:12 -0300 Subject: [PATCH 03/32] build: update addon to 5.0.0-SNAPSHOT Close #57 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 87b4183..e3e5262 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.vaadin.addons.flowingcode chat-assistant-addon - 4.0.1-SNAPSHOT + 5.0.0-SNAPSHOT Chat Assistant Add-on Chat Assistant Add-on for Vaadin Flow From fc3ceeb5fd89b37bf36b50e3d0382e45782bd350 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 11:51:07 -0300 Subject: [PATCH 04/32] WIP: use this instead of document --- .../addons/chatassistant/ChatAssistant.java | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index af00b7a..0caaaf7 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -132,15 +132,15 @@ public ChatAssistant(boolean markdownEnabled) { protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); addComponentRefreshedListener( - "fc-chat-assistant-drag-listener", this, + "fc-chat-assistant-drag-listener", () -> this.getElement().executeJs( "window.fcChatAssistantMovement($0, $1, $2, $3, $4);", this.getElement(), fabWrapper.getElement(), fab.getElement(), DEFAULT_FAB_MARGIN, DEFAULT_DRAG_SENSITIVITY ) ); chatWindow.addOpenedChangeListener(ev -> { - if(ev.isOpened()) { - addComponentRefreshedListener("fc-chat-assistant-resize-top-listener", this, + if (ev.isOpened()) { + addComponentRefreshedListener("fc-chat-assistant-resize-top-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeTop($0, $1, $2, $3, $4);", @@ -149,7 +149,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-bottom-right-listener", this, + "fc-chat-assistant-resize-bottom-right-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeBottomRight($0, $1, $2, $3, $4);", @@ -158,7 +158,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-top-right-listener", this, + "fc-chat-assistant-resize-top-right-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeTopRight($0, $1, $2, $3, $4);", resizerTopRight.getElement(), overlay, @@ -166,7 +166,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-right-listener", this, + "fc-chat-assistant-resize-right-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeRight($0, $1, $2, $3, $4);", resizerRight.getElement(), overlay, @@ -174,7 +174,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-bottom-listener", this, + "fc-chat-assistant-resize-bottom-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeBottom($0, $1, $2, $3, $4);", resizerBottom.getElement(), overlay, @@ -182,7 +182,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-left-listener", this, + "fc-chat-assistant-resize-left-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeLeft($0, $1, $2, $3, $4);", resizerLeft.getElement(), overlay, @@ -190,7 +190,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-top-left-listener", this, + "fc-chat-assistant-resize-top-left-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeTopLeft($0, $1, $2, $3, $4);", resizerTopLeft.getElement(), overlay, @@ -198,7 +198,7 @@ protected void onAttach(AttachEvent attachEvent) { ) ); addComponentRefreshedListener( - "fc-chat-assistant-resize-bottom-left-listener", this, + "fc-chat-assistant-resize-bottom-left-listener", () -> this.getElement().executeJs( "window.fcChatAssistantResizeBottomLeft($0, $1, $2, $3, $4);", resizerBottomLeft.getElement(), overlay, @@ -350,24 +350,21 @@ protected void applyGenericResizerStyle(Div resizer, String direction) { resizer.addClassName(DEFAULT_RESIZE_CLASS + "-" + direction); } + /** - * Adds a listener to the given root component that executes the provided - * callback when the DOM instance of the component is replaced (i.e., when the previous - * client-side JavaScript code attached to the old component disappears and a new instance - * is created). - * This is useful for re-initializing client-side logic after component - * refreshes while preventing listeners from stacking. + * Adds a component refresh listener that prevents stacking up duplicate listeners on the client side. + * Uses a unique flag to track if the listener has already been added for this component instance, + * ensuring the callback only executes once per component refresh cycle. * * @param uniqueFlag a unique identifier for the component instance - * @param root the root component to observe * @param callback the action to execute when the component is refreshed */ - protected static void addComponentRefreshedListener(String uniqueFlag, Component root, Runnable callback) { - root.getElement().executeJs( + protected void addComponentRefreshedListener(String uniqueFlag, Runnable callback) { + this.getElement().executeJs( """ const flag = $0; - if(document[flag] && document[flag] == this) return true; - document[flag] = this; + if(this[flag]) return true; + this[flag] = this; return false; """, uniqueFlag).then(r -> { if (!r.asBoolean()) From 694cbf857e07dae8742c0adef719b7d6a1621108 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 11:55:10 -0300 Subject: [PATCH 05/32] WIP: use default submit listener --- .../vaadin/addons/chatassistant/ChatAssistant.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 0caaaf7..68c5c73 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -523,8 +523,11 @@ public void clearWhoIsTyping() { * @return registration for removal of the listener */ public Registration setSubmitListener(ComponentEventListener listener) { - this.defaultSubmitListenerRegistration.remove(); - return this.messageInput.addSubmitListener(listener); + if(this.defaultSubmitListenerRegistration != null) { + this.defaultSubmitListenerRegistration.remove(); + } + this.defaultSubmitListenerRegistration = this.messageInput.addSubmitListener(listener); + return this.defaultSubmitListenerRegistration; } private void refreshContent() { From e01a391b410fafc86ef8618220d66a0f8068fc2f Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 11:57:22 -0300 Subject: [PATCH 06/32] WIP: use correct unread count --- .../vaadin/addons/chatassistant/ChatAssistant.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 68c5c73..020c91c 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -688,8 +688,8 @@ public int getUnreadMessages() { */ public void setUnreadMessages(int unreadMessages) { this.unreadMessages = unreadMessages >= 0 ? Math.min(unreadMessages, 99) : 0; - unreadBadge.setText(String.valueOf(unreadMessages)); - if(unreadMessages > 0) { + unreadBadge.setText(String.valueOf(this.unreadMessages)); + if(this.unreadMessages > 0) { unreadBadge.getStyle().setScale("1"); } else { From 07ed4c9e9d1a4cb951d68f35986c38e43cdc4ce2 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 12:02:06 -0300 Subject: [PATCH 07/32] WIP: snap to boundary on resizing --- .../resources/META-INF/frontend/fc-chat-assistant-movement.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 39c70ae..00d3c4c 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -37,9 +37,7 @@ window.fcChatAssistantMovement = (root, item, fab, marginRaw, sensitivityRaw) => screenWidth = window.innerWidth; screenHeight = window.innerHeight; // Reposition the item to ensure it stays within the new screen bounds - position.x = margin; - position.y = margin; - updatePosition(); + snapToBoundary(); }); // Update FAB position From f6b6d82822ff9490c26ac5f7ca931dce530066db Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 12:10:00 -0300 Subject: [PATCH 08/32] WIP: remove incorrect newHeight declaration --- .../META-INF/frontend/fc-chat-assistant-resize-bottom-left.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index f3c439e..fdfce2a 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -63,7 +63,6 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, item.style.width = maxSize + 'px'; item.style.marginBottom = -(maxSize / 2) + 'px'; item.style.marginLeft = -(maxSize / 2) + 'px'; - newHeight = offsetY + container.clientHeight; minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; From 3af9691f64f364c7637b6503f2c94f2576c5167a Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 14:49:15 -0300 Subject: [PATCH 09/32] WIP: add pointer capture targets --- .../frontend/fc-chat-assistant-resize-bottom-left.js | 7 +++++-- .../frontend/fc-chat-assistant-resize-bottom-right.js | 7 +++++-- .../META-INF/frontend/fc-chat-assistant-resize-bottom.js | 7 +++++-- .../META-INF/frontend/fc-chat-assistant-resize-left.js | 9 ++++++--- .../META-INF/frontend/fc-chat-assistant-resize-right.js | 7 +++++-- .../frontend/fc-chat-assistant-resize-top-left.js | 9 ++++++--- .../frontend/fc-chat-assistant-resize-top-right.js | 7 +++++-- .../META-INF/frontend/fc-chat-assistant-resize-top.js | 7 +++++-- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index fdfce2a..b172ca9 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -50,6 +50,7 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); @@ -84,19 +85,21 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js index ab511f5..1a55049 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js @@ -49,6 +49,7 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); @@ -83,19 +84,21 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js index 549059e..115d64d 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js @@ -45,6 +45,7 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; } @@ -70,15 +71,17 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginBottom = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginBottom = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js index 05a74c5..6773b53 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -43,9 +43,10 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz return overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; } - item.addEventListener('pointerenter', (_) => { + item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); @@ -71,15 +72,17 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js index 341424c..c708368 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js @@ -45,6 +45,7 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; } @@ -70,15 +71,17 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index 5ba9059..51b0a90 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -43,13 +43,14 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max // Restrict dragging capability to when the popover content has enough space in the corresponding directions function shouldDrag() { const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""); return topRule && leftRule; } item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; @@ -84,19 +85,21 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginLeft = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js index f410353..5c95e73 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js @@ -49,6 +49,7 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; @@ -83,19 +84,21 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginRight = ''; + item.releasePointerCapture(e.pointerId); }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js index d60145b..36565f8 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js @@ -45,6 +45,7 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); + item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; } @@ -70,15 +71,17 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize } }); - item.addEventListener('pointerup', (_) => { + item.addEventListener('pointerup', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginTop = ''; + item.releasePointerCapture(e.pointerId); }); - item.addEventListener('pointerleave', (_) => { + item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginTop = ''; + item.releasePointerCapture(e.pointerId); }); }; From 86df4f24719fa621374fc53cd56cfc32f2aadb60 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 14:50:29 -0300 Subject: [PATCH 10/32] WIP: drop overlay attribute check --- .../META-INF/frontend/fc-chat-assistant-resize-bottom-left.js | 3 +-- .../META-INF/frontend/fc-chat-assistant-resize-left.js | 3 +-- .../META-INF/frontend/fc-chat-assistant-resize-top-left.js | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index b172ca9..921fb1f 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -21,7 +21,6 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, const size = parseFloat(sizeRaw); const maxSize = parseFloat(maxSizeRaw); const overlayTag = "vaadin-popover-overlay".toUpperCase(); - const overlayArrowCenteredAttribute = "arrow-centered"; let minWidth = 0; let minHeight = 0; @@ -43,7 +42,7 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, // Restrict dragging capability to when the popover content has enough space in the corresponding directions function shouldDrag() { const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; - const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + const leftRule = overlay && overlay.style.right && !overlay.style.left; return bottomRule && leftRule; } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js index 6773b53..5be31fb 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -21,7 +21,6 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz const size = parseFloat(sizeRaw); const maxSize = parseFloat(maxSizeRaw); const overlayTag = "vaadin-popover-overlay".toUpperCase(); - const overlayArrowCenteredAttribute = "arrow-centered"; let minWidth = 0; let maxWidth = Infinity; @@ -40,7 +39,7 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz // Restrict dragging capability to when the popover content has enough space in the corresponding direction function shouldDrag() { - return overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""; + return overlay && overlay.style.right && !overlay.style.left; } item.addEventListener('pointerenter', (e) => { diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index 51b0a90..0ba108f 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -21,7 +21,6 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max const size = parseFloat(sizeRaw); const maxSize = parseFloat(maxSizeRaw); const overlayTag = "vaadin-popover-overlay".toUpperCase(); - const overlayArrowCenteredAttribute = "arrow-centered"; let minWidth = 0; let minHeight = 0; @@ -43,7 +42,7 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max // Restrict dragging capability to when the popover content has enough space in the corresponding directions function shouldDrag() { const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const leftRule = overlay && overlay.style.right && overlay.getAttribute(overlayArrowCenteredAttribute) != ""); + const leftRule = overlay && overlay.style.right && !overlay.style.left; return topRule && leftRule; } From 367e0306d6b5655c7ea086bf09ae8a8e5fe2549d Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 14:51:26 -0300 Subject: [PATCH 11/32] WIP: remove unnecessary comment --- .../frontend/styles/fc-chat-assistant-style.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css index e3d3208..6e0884a 100644 --- a/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css +++ b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css @@ -42,13 +42,13 @@ } /* Specific cursors for each corner */ -.fc-chat-assistant-resize-bottom-left.active { cursor: sw-resize; } /* North-West */ -.fc-chat-assistant-resize-bottom-right.active { cursor: se-resize; } /* North-East */ -.fc-chat-assistant-resize-top-left.active { cursor: nw-resize; } /* South-West */ -.fc-chat-assistant-resize-top-right.active { cursor: ne-resize; } /* South-East */ +.fc-chat-assistant-resize-bottom-left.active { cursor: sw-resize; } +.fc-chat-assistant-resize-bottom-right.active { cursor: se-resize; } +.fc-chat-assistant-resize-top-left.active { cursor: nw-resize; } +.fc-chat-assistant-resize-top-right.active { cursor: ne-resize; } /* Specific cursors for the sides */ -.fc-chat-assistant-resize-bottom.active { cursor: s-resize; } /* North/Top */ -.fc-chat-assistant-resize-top.active { cursor: n-resize; } /* South/Bottom */ -.fc-chat-assistant-resize-left.active { cursor: w-resize; } /* West/Left */ -.fc-chat-assistant-resize-right.active { cursor: e-resize; } /* East/Right */ +.fc-chat-assistant-resize-bottom.active { cursor: s-resize; } +.fc-chat-assistant-resize-top.active { cursor: n-resize; } +.fc-chat-assistant-resize-left.active { cursor: w-resize; } +.fc-chat-assistant-resize-right.active { cursor: e-resize; } From 787c85930d8e7ee775feae03ca28dd769261f525 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 15:00:46 -0300 Subject: [PATCH 12/32] WIP: include pointer capture target --- .../META-INF/frontend/fc-chat-assistant-resize-top-left.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index 0ba108f..fe30da6 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -42,7 +42,7 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max // Restrict dragging capability to when the popover content has enough space in the corresponding directions function shouldDrag() { const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const leftRule = overlay && overlay.style.right && !overlay.style.left; + const leftRule = overlay && overlay.style.right && !overlay.hasAttribute(overlayArrowCenteredAttribute); return topRule && leftRule; } From 75ec896ad67c06f19c23aabf2cbaf5b0ad05d76a Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 15:01:28 -0300 Subject: [PATCH 13/32] WIP: bring back chat buttons --- .../vaadin/addons/chatassistant/ChatAssistantDemo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java index b1fa3e6..c3a4e73 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java @@ -104,6 +104,6 @@ public void run() { chatAssistant.setUnreadMessages(4); - add(chatAssistant); + add(message, chat, chatWithThinking, chatAssistant); } } From e51c6fcfde9d4f95bb5b6763009689f2fdf68367 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 15:24:06 -0300 Subject: [PATCH 14/32] WIP: adjusts window size when resizing screen --- .../addons/chatassistant/ChatAssistant.java | 4 +-- .../frontend/fc-chat-assistant-movement.js | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 020c91c..94ed816 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -134,8 +134,8 @@ protected void onAttach(AttachEvent attachEvent) { addComponentRefreshedListener( "fc-chat-assistant-drag-listener", () -> this.getElement().executeJs( - "window.fcChatAssistantMovement($0, $1, $2, $3, $4);", - this.getElement(), fabWrapper.getElement(), fab.getElement(), DEFAULT_FAB_MARGIN, DEFAULT_DRAG_SENSITIVITY + "window.fcChatAssistantMovement($0, $1, $2, $3, $4, $5);", + this.getElement(), fabWrapper.getElement(), overlay, fab.getElement(), DEFAULT_FAB_MARGIN, DEFAULT_DRAG_SENSITIVITY ) ); chatWindow.addOpenedChangeListener(ev -> { diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 00d3c4c..5e76786 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -window.fcChatAssistantMovement = (root, item, fab, marginRaw, sensitivityRaw) => { +window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensitivityRaw) => { const itemWidth = fab.clientWidth; const itemHeight = fab.clientHeight; const margin = parseFloat(marginRaw); @@ -36,6 +36,35 @@ window.fcChatAssistantMovement = (root, item, fab, marginRaw, sensitivityRaw) => window.addEventListener("resize", (_) => { screenWidth = window.innerWidth; screenHeight = window.innerHeight; + + // Adjust container dimensions to fit within screen bounds + if (container) { + const rect = container.getBoundingClientRect(); + let widthAdjustment = 0; + let heightAdjustment = 0; + if (rect.left < 0) { + widthAdjustment = Math.abs(rect.left); + } + if (rect.right > screenWidth) { + widthAdjustment = Math.max(widthAdjustment, rect.right - screenWidth); + } + if (rect.top < 0) { + heightAdjustment = Math.abs(rect.top); + } + if (rect.bottom > screenHeight) { + heightAdjustment = Math.max(heightAdjustment, rect.bottom - screenHeight); + } + // Apply adjustments + if (widthAdjustment > 0) { + const newWidth = Math.max(0, rect.width - widthAdjustment); + container.style.width = newWidth + 'px'; + } + if (heightAdjustment > 0) { + const newHeight = Math.max(0, rect.height - heightAdjustment); + container.style.height = newHeight + 'px'; + } + } + // Reposition the item to ensure it stays within the new screen bounds snapToBoundary(); }); From 96ab550194e718f3e7b13e66486dad7822fa42b0 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 15:35:25 -0300 Subject: [PATCH 15/32] WIP: center mouse movement in fab --- .../resources/META-INF/frontend/fc-chat-assistant-movement.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 5e76786..23c2e1f 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -106,8 +106,8 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti item.addEventListener('pointermove', (e) => { if (!isDragging) return; - const offsetX = screenWidth - e.clientX - itemWidth / 2; - const offsetY = screenHeight - e.clientY - itemHeight / 2; + const offsetX = screenWidth - e.clientX - itemWidth; + const offsetY = screenHeight - e.clientY - itemHeight; position.x = offsetX; position.y = offsetY; updatePosition(); From ed1383e27f2e1154cb49d4ba08c29ce3bf977c60 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 15:37:21 -0300 Subject: [PATCH 16/32] WIP: add fc- prefix to message css style --- .../flowingcode/vaadin/addons/chatassistant/ChatMessage.java | 2 +- .../{chat-message-styles.css => fc-chat-message-styles.css} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/main/resources/META-INF/frontend/styles/{chat-message-styles.css => fc-chat-message-styles.css} (100%) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java index f1e4582..1fd1b7e 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java @@ -38,7 +38,7 @@ @SuppressWarnings("serial") @JsModule("@vaadin/message-list/src/vaadin-message.js") @Tag("vaadin-message") -@CssImport("./styles/chat-message-styles.css") +@CssImport("./styles/fc-chat-message-styles.css") @EqualsAndHashCode(callSuper=false) public class ChatMessage extends Component implements HasComponents { diff --git a/src/main/resources/META-INF/frontend/styles/chat-message-styles.css b/src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css similarity index 100% rename from src/main/resources/META-INF/frontend/styles/chat-message-styles.css rename to src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css From 3e3e204f4f5b4032a3d3b4067b102989d9452685 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 16:54:03 -0300 Subject: [PATCH 17/32] WIP: initialize unread count to variable value --- .../vaadin/addons/chatassistant/ChatAssistant.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 94ed816..c560d25 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -107,7 +107,7 @@ public class ChatAssistant extends Div { private MessageInput messageInput; private Span whoIsTyping; private Registration defaultSubmitListenerRegistration; - private int unreadMessages; + private int unreadMessages = 0; public ChatAssistant(List messages, boolean markdownEnabled) { this.setUI(); @@ -240,7 +240,7 @@ private void setUI() { .setJustifyContent(Style.JustifyContent.CENTER) .setPosition(Style.Position.FIXED); - unreadBadge.setText(String.valueOf(99)); + unreadBadge.setText(String.valueOf(unreadMessages)); unreadBadge.addClassName(DEFAULT_UNREAD_BADGE_CLASS); unreadBadge.getStyle() .setTextAlign(Style.TextAlign.CENTER) From 02a4928436f08cc7c8d2128d308ee6d0479562d5 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 16:56:39 -0300 Subject: [PATCH 18/32] WIP: consider minimum dimensions when shrinking content window --- .../META-INF/frontend/fc-chat-assistant-movement.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 23c2e1f..2ab04fb 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -56,11 +56,13 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti } // Apply adjustments if (widthAdjustment > 0) { - const newWidth = Math.max(0, rect.width - widthAdjustment); + const minWidth = parseFloat(container.style.minWidth) || 0; + const newWidth = Math.max(minWidth, rect.width - widthAdjustment); container.style.width = newWidth + 'px'; } if (heightAdjustment > 0) { - const newHeight = Math.max(0, rect.height - heightAdjustment); + const minHeight = parseFloat(container.style.minHeight) || 0; + const newHeight = Math.max(minHeight, rect.height - heightAdjustment); container.style.height = newHeight + 'px'; } } From 8651263aec2937af494a2c37027359fa825b6827 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 16:57:28 -0300 Subject: [PATCH 19/32] WIP: guard against null server reference --- .../resources/META-INF/frontend/fc-chat-assistant-movement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 2ab04fb..060e2f2 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -122,7 +122,7 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti item.releasePointerCapture(e.pointerId); snapToBoundary(); if (isClickOnlyEvent()) { - root.$server.onClick(); + root.$server?.onClick(); } }); From f13f050bfb6729697eb671c9eb7c6fd4479e9318 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 16:59:53 -0300 Subject: [PATCH 20/32] WIP: guard against invalid pointer capture target --- .../frontend/fc-chat-assistant-resize-bottom-left.js | 8 ++++++-- .../frontend/fc-chat-assistant-resize-bottom-right.js | 8 ++++++-- .../META-INF/frontend/fc-chat-assistant-resize-bottom.js | 8 ++++++-- .../META-INF/frontend/fc-chat-assistant-resize-left.js | 8 ++++++-- .../META-INF/frontend/fc-chat-assistant-resize-right.js | 8 ++++++-- .../frontend/fc-chat-assistant-resize-top-left.js | 8 ++++++-- .../frontend/fc-chat-assistant-resize-top-right.js | 8 ++++++-- .../META-INF/frontend/fc-chat-assistant-resize-top.js | 8 ++++++-- 8 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index 921fb1f..53d90d9 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -90,7 +90,9 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { @@ -99,6 +101,8 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js index 1a55049..819ddcd 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js @@ -90,7 +90,9 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { @@ -99,6 +101,8 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, item.style.width = size + 'px'; item.style.marginBottom = ''; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js index 115d64d..4d001c3 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js @@ -75,13 +75,17 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS isDragging = false; item.style.height = size + 'px'; item.style.marginBottom = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginBottom = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js index 5be31fb..790c0c0 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -75,13 +75,17 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz isDragging = false; item.style.width = size + 'px'; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js index c708368..50000eb 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js @@ -75,13 +75,17 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi isDragging = false; item.style.width = size + 'px'; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.width = size + 'px'; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index fe30da6..b94df25 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -90,7 +90,9 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { @@ -99,6 +101,8 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginLeft = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js index 5c95e73..2faf08f 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js @@ -90,7 +90,9 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { @@ -99,6 +101,8 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma item.style.width = size + 'px'; item.style.marginTop = ''; item.style.marginRight = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js index 36565f8..4a98d30 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js @@ -75,13 +75,17 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize isDragging = false; item.style.height = size + 'px'; item.style.marginTop = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); item.addEventListener('pointerleave', (e) => { isDragging = false; item.style.height = size + 'px'; item.style.marginTop = ''; - item.releasePointerCapture(e.pointerId); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } }); }; From 44804bde8e68c3ba05f25586dbb468a0d2d84f76 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:01:27 -0300 Subject: [PATCH 21/32] WIP: remove non existing overlayArrowCenteredAttribute variable usage --- .../META-INF/frontend/fc-chat-assistant-resize-top-left.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index b94df25..5671d02 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -42,7 +42,7 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max // Restrict dragging capability to when the popover content has enough space in the corresponding directions function shouldDrag() { const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const leftRule = overlay && overlay.style.right && !overlay.hasAttribute(overlayArrowCenteredAttribute); + const leftRule = overlay && overlay.style.right && !overlay.style.left; return topRule && leftRule; } From cea3b166357317c599339a45f2269e39c34d6d76 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:04:46 -0300 Subject: [PATCH 22/32] WIP: drop unnecessary server round trip when refreshing listeners --- .../addons/chatassistant/ChatAssistant.java | 104 ++++++++---------- 1 file changed, 45 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index c560d25..eebb5c7 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -50,6 +50,7 @@ import com.vaadin.flow.function.SerializableSupplier; import com.vaadin.flow.shared.Registration; +import java.io.Serializable; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -133,77 +134,61 @@ protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); addComponentRefreshedListener( "fc-chat-assistant-drag-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantMovement($0, $1, $2, $3, $4, $5);", - this.getElement(), fabWrapper.getElement(), overlay, fab.getElement(), DEFAULT_FAB_MARGIN, DEFAULT_DRAG_SENSITIVITY - ) + "window.fcChatAssistantMovement($0, $1, $2, $3, $4, $5);", + this.getElement(), fabWrapper.getElement(), overlay, fab.getElement(), DEFAULT_FAB_MARGIN, + DEFAULT_DRAG_SENSITIVITY + ); chatWindow.addOpenedChangeListener(ev -> { if (ev.isOpened()) { - addComponentRefreshedListener("fc-chat-assistant-resize-top-listener", - () -> - this.getElement().executeJs( - "window.fcChatAssistantResizeTop($0, $1, $2, $3, $4);", - resizerTop.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + addComponentRefreshedListener( + "fc-chat-assistant-resize-top-listener", + "window.fcChatAssistantResizeTop($0, $1, $2, $3, $4);", + resizerTop.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE + ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-right-listener", - () -> - this.getElement().executeJs( - "window.fcChatAssistantResizeBottomRight($0, $1, $2, $3, $4);", - resizerBottomRight.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeBottomRight($0, $1, $2, $3, $4);", + resizerBottomRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-top-right-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeTopRight($0, $1, $2, $3, $4);", - resizerTopRight.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeTopRight($0, $1, $2, $3, $4);", + resizerTopRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-right-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeRight($0, $1, $2, $3, $4);", - resizerRight.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeRight($0, $1, $2, $3, $4);", + resizerRight.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeBottom($0, $1, $2, $3, $4);", - resizerBottom.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeBottom($0, $1, $2, $3, $4);", + resizerBottom.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-left-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeLeft($0, $1, $2, $3, $4);", - resizerLeft.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeLeft($0, $1, $2, $3, $4);", + resizerLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-top-left-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeTopLeft($0, $1, $2, $3, $4);", - resizerTopLeft.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeTopLeft($0, $1, $2, $3, $4);", + resizerTopLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-left-listener", - () -> this.getElement().executeJs( - "window.fcChatAssistantResizeBottomLeft($0, $1, $2, $3, $4);", - resizerBottomLeft.getElement(), overlay, - DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE - ) + "window.fcChatAssistantResizeBottomLeft($0, $1, $2, $3, $4);", + resizerBottomLeft.getElement(), overlay, + DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); } }); @@ -356,20 +341,21 @@ protected void applyGenericResizerStyle(Div resizer, String direction) { * Uses a unique flag to track if the listener has already been added for this component instance, * ensuring the callback only executes once per component refresh cycle. * - * @param uniqueFlag a unique identifier for the component instance - * @param callback the action to execute when the component is refreshed + * @param uniqueFlag a unique identifier for the component instance + * @param executable the JavaScript action to execute when the component is refreshed, + * @param parameters parameters for the executable */ - protected void addComponentRefreshedListener(String uniqueFlag, Runnable callback) { + protected void addComponentRefreshedListener(String uniqueFlag, String executable, Serializable... parameters) { this.getElement().executeJs( - """ - const flag = $0; - if(this[flag]) return true; - this[flag] = this; - return false; - """, uniqueFlag).then(r -> { - if (!r.asBoolean()) - callback.run(); - }); + String.format( + """ + if(!this['%1$s']) { %2$s } + else { + this['%1$s'] = this; + }; + """, uniqueFlag, executable), + parameters + ); } /** Sets the icon for the floating action button. From 93311ca33bcc8feb9183351c30b4eed87228e4f0 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:05:36 -0300 Subject: [PATCH 23/32] WIP: return to previous movement offset --- .../resources/META-INF/frontend/fc-chat-assistant-movement.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 060e2f2..6740b13 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -108,8 +108,8 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti item.addEventListener('pointermove', (e) => { if (!isDragging) return; - const offsetX = screenWidth - e.clientX - itemWidth; - const offsetY = screenHeight - e.clientY - itemHeight; + const offsetX = screenWidth - e.clientX - itemWidth / 2; + const offsetY = screenHeight - e.clientY - itemHeight / 2; position.x = offsetX; position.y = offsetY; updatePosition(); From 38aa323b47219831cf6994a05dc4c654308d2035 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:27:54 -0300 Subject: [PATCH 24/32] WIP: implement other mechanism to get popover overlay --- .../frontend/fc-chat-assistant-resize-bottom-left.js | 5 ++++- .../frontend/fc-chat-assistant-resize-bottom-right.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-bottom.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-left.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-right.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-top-left.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-top-right.js | 5 ++++- .../META-INF/frontend/fc-chat-assistant-resize-top.js | 5 ++++- 8 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index 53d90d9..678adb5 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -35,7 +35,10 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js index 819ddcd..4ee28b5 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js @@ -35,7 +35,10 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js index 4d001c3..628bf78 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js @@ -33,7 +33,10 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js index 790c0c0..70d4e99 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -33,7 +33,10 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js index 50000eb..4ba70ee 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js @@ -33,7 +33,10 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index 5671d02..4f41e51 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -35,7 +35,10 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js index 2faf08f..eb9cb0c 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js @@ -35,7 +35,10 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js index 4a98d30..773a933 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js @@ -33,7 +33,10 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize // Fetch the root overlay component function fetchOverlay() { if (!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } } } From 6d44d6d349e285c1284a9eda1b9236382d1b1bf1 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:28:31 -0300 Subject: [PATCH 25/32] WIP: use current width and height when moving fab --- .../frontend/fc-chat-assistant-movement.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 6740b13..1daaa0c 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -18,8 +18,6 @@ * #L% */ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensitivityRaw) => { - const itemWidth = fab.clientWidth; - const itemHeight = fab.clientHeight; const margin = parseFloat(marginRaw); const sensitivity = parseFloat(sensitivityRaw); const sizeTransition = 'transform 0.2s ease'; @@ -79,8 +77,11 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti // Ensure the item stays within the screen and margin bounds function snapToBoundary() { - const xMax = screenWidth - itemWidth - margin; - const yMax = screenHeight - itemHeight - margin; + // Get current dimensions to account for transforms + const itemRect = fab.getBoundingClientRect(); + + const xMax = screenWidth - itemRect.width - margin; + const yMax = screenHeight - itemRect.height - margin; const x = position.x; const y = position.y; if (x < margin) position.x = margin; @@ -108,10 +109,11 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti item.addEventListener('pointermove', (e) => { if (!isDragging) return; - const offsetX = screenWidth - e.clientX - itemWidth / 2; - const offsetY = screenHeight - e.clientY - itemHeight / 2; - position.x = offsetX; - position.y = offsetY; + const itemRect = fab.getBoundingClientRect(); + // Calculate position from right and bottom edges + position.x = screenWidth - e.clientX - (itemRect.width / 2); + position.y = screenHeight - e.clientY - (itemRect.height / 2); + updatePosition(); }); From 198081b92fbf52c422a9b28995bec07e24286511 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:33:24 -0300 Subject: [PATCH 26/32] WIP: fix license format in styles --- .../META-INF/frontend/styles/fc-chat-assistant-style.css | 2 +- .../META-INF/frontend/styles/fc-chat-message-styles.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css index 6e0884a..8468fcc 100644 --- a/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css +++ b/src/main/resources/META-INF/frontend/styles/fc-chat-assistant-style.css @@ -1,4 +1,4 @@ -/*- +/* * #%L * Chat Assistant Add-on * %% diff --git a/src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css b/src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css index fbc737c..03712b9 100644 --- a/src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css +++ b/src/main/resources/META-INF/frontend/styles/fc-chat-message-styles.css @@ -1,4 +1,4 @@ -/*- +/* * #%L * Chat Assistant Add-on * %% @@ -101,4 +101,4 @@ .language-mermaid { padding: 0px !important; -} \ No newline at end of file +} From 91728df8eb9e8463cf71e7c471316cd477cfac2a Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:34:17 -0300 Subject: [PATCH 27/32] build: remove json-migration-helper --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index e3e5262..e88be35 100644 --- a/pom.xml +++ b/pom.xml @@ -124,11 +124,6 @@ markdown-editor-addon ${markdown-editor.version} - - com.flowingcode.vaadin - json-migration-helper - 0.9.2 - org.projectlombok lombok From 5fe029fad711244b692ad9d298ae21d7e7c15540 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:49:01 -0300 Subject: [PATCH 28/32] WIP: fix listener refresh flag --- .../flowingcode/vaadin/addons/chatassistant/ChatAssistant.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index eebb5c7..0c1ec8b 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -350,7 +350,7 @@ protected void addComponentRefreshedListener(String uniqueFlag, String executabl String.format( """ if(!this['%1$s']) { %2$s } - else { + if(!this['%1$s']) { this['%1$s'] = this; }; """, uniqueFlag, executable), From 521605db036b2522e53d53ddaa820d79c46ff307 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Wed, 11 Mar 2026 17:50:37 -0300 Subject: [PATCH 29/32] WIP: usage uniqueFlag instead of object reference --- .../flowingcode/vaadin/addons/chatassistant/ChatAssistant.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 0c1ec8b..adc5a82 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -351,7 +351,7 @@ protected void addComponentRefreshedListener(String uniqueFlag, String executabl """ if(!this['%1$s']) { %2$s } if(!this['%1$s']) { - this['%1$s'] = this; + this['%1$s'] = '%1$s'; }; """, uniqueFlag, executable), parameters From a6a41e2bb3cbb6ede48a72db461125b8ec4ec944 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Thu, 12 Mar 2026 10:48:04 -0300 Subject: [PATCH 30/32] WIP: improve pointer event handling --- .../addons/chatassistant/ChatAssistant.java | 2 +- .../frontend/fc-chat-assistant-movement.js | 24 ++++++++++++------- .../fc-chat-assistant-resize-bottom-left.js | 24 +++++++------------ .../fc-chat-assistant-resize-bottom-right.js | 24 +++++++------------ .../fc-chat-assistant-resize-bottom.js | 22 +++++++---------- .../frontend/fc-chat-assistant-resize-left.js | 22 +++++++---------- .../fc-chat-assistant-resize-right.js | 22 +++++++---------- .../fc-chat-assistant-resize-top-left.js | 24 +++++++------------ .../fc-chat-assistant-resize-top-right.js | 24 +++++++------------ .../frontend/fc-chat-assistant-resize-top.js | 20 +++++++--------- 10 files changed, 87 insertions(+), 121 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index adc5a82..be2683c 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -91,7 +91,7 @@ public class ChatAssistant extends Div { protected static final int DEFAULT_FAB_ICON_SIZE = 45; protected static final int DEFAULT_FAB_MARGIN = 25; protected static final int DEFAULT_RESIZER_SIZE = 25; - protected static final int DEFAULT_MAX_RESIZER_SIZE = 150; + protected static final int DEFAULT_MAX_RESIZER_SIZE = 200; protected static final int DEFAULT_DRAG_SENSITIVITY = 10; private static final int DEFAULT_CONTENT_MIN_WIDTH = 150; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js index 1daaa0c..bba1ad7 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-movement.js @@ -117,16 +117,22 @@ window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensiti updatePosition(); }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.transition = snapTransition + ', ' + sizeTransition; - fab.classList.remove('dragging'); - item.releasePointerCapture(e.pointerId); - snapToBoundary(); - if (isClickOnlyEvent()) { - root.$server?.onClick(); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); + + function stopDragging(e) { + if(isDragging) { + isDragging = false; + item.style.transition = snapTransition + ', ' + sizeTransition; + fab.classList.remove('dragging'); + item.releasePointerCapture(e.pointerId); + snapToBoundary(); + if (isClickOnlyEvent()) { + root.$server?.onClick(); + } } - }); + } updatePosition(); }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js index 678adb5..4cd625f 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js @@ -49,19 +49,19 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, return bottomRule && leftRule; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.width = maxSize + 'px'; item.style.marginBottom = -(maxSize / 2) + 'px'; @@ -87,19 +87,13 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginBottom = ''; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; @@ -107,5 +101,5 @@ window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js index 4ee28b5..a3cc356 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js @@ -49,19 +49,19 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, return bottomRule && rightRule; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.width = maxSize + 'px'; item.style.marginBottom = -(maxSize / 2) + 'px'; @@ -87,19 +87,13 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginBottom = ''; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginBottom = ''; @@ -107,5 +101,5 @@ window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js index 628bf78..925cfcd 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js @@ -45,10 +45,9 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS return overlay && !overlay.style.bottom && overlay.style.top; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; } @@ -57,9 +56,10 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.marginBottom = -(maxSize / 2) + 'px'; } @@ -74,21 +74,17 @@ window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxS } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.marginBottom = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.marginBottom = ''; if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js index 70d4e99..8d8562c 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js @@ -45,19 +45,19 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz return overlay && overlay.style.right && !overlay.style.left; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); } else { item.classList.remove('active'); } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.width = maxSize + 'px'; item.style.marginLeft = -(maxSize / 2) + 'px'; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; @@ -74,21 +74,17 @@ window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSiz } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.width = size + 'px'; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.width = size + 'px'; item.style.marginLeft = ''; if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js index 4ba70ee..3a022dc 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js @@ -45,10 +45,9 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi return overlay && overlay.style.left && !overlay.style.right; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; } @@ -57,9 +56,10 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.width = maxSize + 'px'; item.style.marginRight = -(maxSize / 2) + 'px'; } @@ -74,21 +74,17 @@ window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSi } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.width = size + 'px'; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.width = size + 'px'; item.style.marginRight = ''; if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js index 4f41e51..3833afe 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js @@ -49,10 +49,9 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max return topRule && leftRule; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; @@ -63,9 +62,10 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.width = maxSize + 'px'; item.style.marginTop = -(maxSize / 2) + 'px'; @@ -87,19 +87,13 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginTop = ''; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; @@ -107,5 +101,5 @@ window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, max if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js index eb9cb0c..3d963bd 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js @@ -49,10 +49,9 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma return topRule && rightRule; } - item.addEventListener('pointerenter', (e) => { + item.addEventListener('pointerenter', (_) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; @@ -63,9 +62,10 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.width = maxSize + 'px'; item.style.marginTop = -(maxSize / 2) + 'px'; @@ -87,19 +87,13 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginTop = ''; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.width = size + 'px'; item.style.marginTop = ''; @@ -107,5 +101,5 @@ window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, ma if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js index 773a933..f9a7869 100644 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js @@ -48,7 +48,6 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize item.addEventListener('pointerenter', (e) => { if (shouldDrag()) { item.classList.add('active'); - item.setPointerCapture(e.pointerId); minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; } @@ -57,9 +56,10 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize } }); - item.addEventListener('pointerdown', (_) => { + item.addEventListener('pointerdown', (e) => { isDragging = shouldDrag(); if (isDragging) { + item.setPointerCapture(e.pointerId); item.style.height = maxSize + 'px'; item.style.marginTop = -(maxSize / 2) + 'px'; } @@ -74,21 +74,17 @@ window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSize } }); - item.addEventListener('pointerup', (e) => { - isDragging = false; - item.style.height = size + 'px'; - item.style.marginTop = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - }); + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => { + function stopDragging(e) { isDragging = false; + item.classList.remove('active'); item.style.height = size + 'px'; item.style.marginTop = ''; if (item.hasPointerCapture(e.pointerId)) { item.releasePointerCapture(e.pointerId); } - }); + } }; From fa3dc9302887ed0d7770c6da20225394db2d0844 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Thu, 12 Mar 2026 10:59:55 -0300 Subject: [PATCH 31/32] WIP: fix resizers positions --- .../vaadin/addons/chatassistant/ChatAssistant.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index be2683c..68c4bdf 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -258,18 +258,21 @@ private void setUI() { applyGenericResizerStyle(resizerTop, "top"); resizerTop.getStyle() .setTop("0") + .setLeft("0") .setHeight(DEFAULT_RESIZER_SIZE + "px") .setWidth("100%"); applyGenericResizerStyle(resizerBottom, "bottom"); resizerBottom.getStyle() .setBottom("0") + .setLeft("0") .setHeight(DEFAULT_RESIZER_SIZE + "px") .setWidth("100%"); applyGenericResizerStyle(resizerTopRight, "top-right"); resizerTopRight.getStyle() .setRight("0") + .setTop("0") .setHeight(DEFAULT_RESIZER_SIZE + "px") .setWidth(DEFAULT_RESIZER_SIZE + "px"); @@ -282,12 +285,14 @@ private void setUI() { applyGenericResizerStyle(resizerRight, "right"); resizerRight.getStyle() + .setTop("0") .setRight("0") .setHeight("100%") .setWidth(DEFAULT_RESIZER_SIZE + "px"); applyGenericResizerStyle(resizerLeft, "left"); resizerLeft.getStyle() + .setTop("0") .setLeft("0") .setHeight("100%") .setWidth(DEFAULT_RESIZER_SIZE + "px"); From 2d7ad6a4c1dffadcf40ea7bc321a63a6ebde3a83 Mon Sep 17 00:00:00 2001 From: Ezequiel M Izaguirre Date: Thu, 12 Mar 2026 11:32:19 -0300 Subject: [PATCH 32/32] WIP: combine resizing logic into single file and method --- .../addons/chatassistant/ChatAssistant.java | 25 +- .../fc-chat-assistant-resize-bottom-left.js | 105 ------- .../fc-chat-assistant-resize-bottom-right.js | 105 ------- .../fc-chat-assistant-resize-bottom.js | 90 ------ .../frontend/fc-chat-assistant-resize-left.js | 90 ------ .../fc-chat-assistant-resize-right.js | 90 ------ .../fc-chat-assistant-resize-top-left.js | 105 ------- .../fc-chat-assistant-resize-top-right.js | 105 ------- .../frontend/fc-chat-assistant-resize-top.js | 90 ------ .../frontend/fc-chat-assistant-resize.js | 290 ++++++++++++++++++ 10 files changed, 299 insertions(+), 796 deletions(-) delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js delete mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js create mode 100644 src/main/resources/META-INF/frontend/fc-chat-assistant-resize.js diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 68c4bdf..98f3a60 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -57,14 +57,7 @@ import java.util.Objects; @JsModule("./fc-chat-assistant-movement.js") -@JsModule("./fc-chat-assistant-resize-top.js") -@JsModule("./fc-chat-assistant-resize-top-right.js") -@JsModule("./fc-chat-assistant-resize-right.js") -@JsModule("./fc-chat-assistant-resize-bottom-right.js") -@JsModule("./fc-chat-assistant-resize-bottom.js") -@JsModule("./fc-chat-assistant-resize-left.js") -@JsModule("./fc-chat-assistant-resize-bottom-left.js") -@JsModule("./fc-chat-assistant-resize-top-left.js") +@JsModule("./fc-chat-assistant-resize.js") @CssImport("./styles/fc-chat-assistant-style.css") @Tag("animated-fab") public class ChatAssistant extends Div { @@ -143,50 +136,50 @@ protected void onAttach(AttachEvent attachEvent) { if (ev.isOpened()) { addComponentRefreshedListener( "fc-chat-assistant-resize-top-listener", - "window.fcChatAssistantResizeTop($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'top');", resizerTop.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-right-listener", - "window.fcChatAssistantResizeBottomRight($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'bottom-right');", resizerBottomRight.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-top-right-listener", - "window.fcChatAssistantResizeTopRight($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'top-right');", resizerTopRight.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-right-listener", - "window.fcChatAssistantResizeRight($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'right');", resizerRight.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-listener", - "window.fcChatAssistantResizeBottom($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'bottom');", resizerBottom.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-left-listener", - "window.fcChatAssistantResizeLeft($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'left');", resizerLeft.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-top-left-listener", - "window.fcChatAssistantResizeTopLeft($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'top-left');", resizerTopLeft.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); addComponentRefreshedListener( "fc-chat-assistant-resize-bottom-left-listener", - "window.fcChatAssistantResizeBottomLeft($0, $1, $2, $3, $4);", + "window.fcChatAssistantResize($0, $1, $2, $3, $4, 'bottom-left');", resizerBottomLeft.getElement(), overlay, DEFAULT_POPOVER_TAG, DEFAULT_RESIZER_SIZE, DEFAULT_MAX_RESIZER_SIZE ); diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js deleted file mode 100644 index 4cd625f..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-left.js +++ /dev/null @@ -1,105 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeBottomLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let minHeight = 0; - let maxWidth = Infinity; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding directions - function shouldDrag() { - const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; - const leftRule = overlay && overlay.style.right && !overlay.style.left; - return bottomRule && leftRule; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.width = maxSize + 'px'; - item.style.marginBottom = -(maxSize / 2) + 'px'; - item.style.marginLeft = -(maxSize / 2) + 'px'; - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = e.clientY - container.getBoundingClientRect().bottom; - const newHeight = offsetY + container.clientHeight; - if(newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - const offsetX = container.getBoundingClientRect().left - e.clientX; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginBottom = ''; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js deleted file mode 100644 index a3cc356..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom-right.js +++ /dev/null @@ -1,105 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeBottomRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let minHeight = 0; - let maxWidth = Infinity; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding directions - function shouldDrag() { - const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; - const rightRule = overlay && overlay.style.left && !overlay.style.right; - return bottomRule && rightRule; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.width = maxSize + 'px'; - item.style.marginBottom = -(maxSize / 2) + 'px'; - item.style.marginRight = -(maxSize / 2) + 'px'; - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = e.clientY - container.getBoundingClientRect().bottom; - const newHeight = offsetY + container.clientHeight; - if (newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - const offsetX = e.clientX - container.getBoundingClientRect().right; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginBottom = ''; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js deleted file mode 100644 index 925cfcd..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-bottom.js +++ /dev/null @@ -1,90 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeBottom = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minHeight = 0; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding direction - function shouldDrag() { - return overlay && !overlay.style.bottom && overlay.style.top; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.marginBottom = -(maxSize / 2) + 'px'; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = e.clientY - container.getBoundingClientRect().bottom; - const newHeight = offsetY + container.clientHeight; - if (newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.marginBottom = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js deleted file mode 100644 index 8d8562c..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-left.js +++ /dev/null @@ -1,90 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let maxWidth = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding direction - function shouldDrag() { - return overlay && overlay.style.right && !overlay.style.left; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.width = maxSize + 'px'; - item.style.marginLeft = -(maxSize / 2) + 'px'; - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetX = container.getBoundingClientRect().left - e.clientX; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.width = size + 'px'; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js deleted file mode 100644 index 3a022dc..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-right.js +++ /dev/null @@ -1,90 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let maxWidth = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding direction - function shouldDrag() { - return overlay && overlay.style.left && !overlay.style.right; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.width = maxSize + 'px'; - item.style.marginRight = -(maxSize / 2) + 'px'; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetX = e.clientX - container.getBoundingClientRect().right; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.width = size + 'px'; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js deleted file mode 100644 index 3833afe..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-left.js +++ /dev/null @@ -1,105 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeTopLeft = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let minHeight = 0; - let maxWidth = Infinity; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding directions - function shouldDrag() { - const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const leftRule = overlay && overlay.style.right && !overlay.style.left; - return topRule && leftRule; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.width = maxSize + 'px'; - item.style.marginTop = -(maxSize / 2) + 'px'; - item.style.marginLeft = -(maxSize / 2) + 'px'; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = container.getBoundingClientRect().top - e.clientY; - const newHeight = offsetY + container.clientHeight; - if(newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - const offsetX = container.getBoundingClientRect().left - e.clientX; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginTop = ''; - item.style.marginLeft = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js deleted file mode 100644 index 3d963bd..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top-right.js +++ /dev/null @@ -1,105 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeTopRight = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minWidth = 0; - let minHeight = 0; - let maxWidth = Infinity; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding directions - function shouldDrag() { - const topRule = overlay && overlay.style.bottom && !overlay.style.top; - const rightRule = overlay && overlay.style.left && !overlay.style.right; - return topRule && rightRule; - } - - item.addEventListener('pointerenter', (_) => { - if (shouldDrag()) { - item.classList.add('active'); - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; - maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.width = maxSize + 'px'; - item.style.marginTop = -(maxSize / 2) + 'px'; - item.style.marginRight = -(maxSize / 2) + 'px'; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = container.getBoundingClientRect().top - e.clientY; - const newHeight = offsetY + container.clientHeight; - if(newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - const offsetX = e.clientX - container.getBoundingClientRect().right; - const newWidth = offsetX + container.clientWidth; - if (newWidth >= minWidth && newWidth <= maxWidth) { - container.style.width = newWidth + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.width = size + 'px'; - item.style.marginTop = ''; - item.style.marginRight = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js deleted file mode 100644 index f9a7869..0000000 --- a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize-top.js +++ /dev/null @@ -1,90 +0,0 @@ -/*- - * #%L - * Chat Assistant Add-on - * %% - * Copyright (C) 2023 - 2026 Flowing Code - * %% - * 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. - * #L% - */ -window.fcChatAssistantResizeTop = (item, container, popoverTag, sizeRaw, maxSizeRaw) => { - const size = parseFloat(sizeRaw); - const maxSize = parseFloat(maxSizeRaw); - const overlayTag = "vaadin-popover-overlay".toUpperCase(); - - let minHeight = 0; - let maxHeight = Infinity; - let overlay; - let isDragging = false; - - window.requestAnimationFrame(fetchOverlay); - setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds - - // Fetch the root overlay component - function fetchOverlay() { - if (!overlay) { - overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); - if(!overlay) { - overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); - } - } - } - - // Restrict dragging capability to when the popover content has enough space in the corresponding direction - function shouldDrag() { - return overlay && overlay.style.bottom && !overlay.style.top; - } - - item.addEventListener('pointerenter', (e) => { - if (shouldDrag()) { - item.classList.add('active'); - minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; - maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; - } - else { - item.classList.remove('active'); - } - }); - - item.addEventListener('pointerdown', (e) => { - isDragging = shouldDrag(); - if (isDragging) { - item.setPointerCapture(e.pointerId); - item.style.height = maxSize + 'px'; - item.style.marginTop = -(maxSize / 2) + 'px'; - } - }); - - item.addEventListener('pointermove', (e) => { - if (!isDragging) return; - const offsetY = container.getBoundingClientRect().top - e.clientY; - const newHeight = offsetY + container.clientHeight; - if (newHeight >= minHeight && newHeight <= maxHeight) { - container.style.height = newHeight + 'px'; - } - }); - - item.addEventListener('pointerup', (e) => stopDragging(e)); - item.addEventListener('pointerleave', (e) => stopDragging(e)); - item.addEventListener('pointercancel', (e) => stopDragging(e)); - - function stopDragging(e) { - isDragging = false; - item.classList.remove('active'); - item.style.height = size + 'px'; - item.style.marginTop = ''; - if (item.hasPointerCapture(e.pointerId)) { - item.releasePointerCapture(e.pointerId); - } - } -}; diff --git a/src/main/resources/META-INF/frontend/fc-chat-assistant-resize.js b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize.js new file mode 100644 index 0000000..e7d84a2 --- /dev/null +++ b/src/main/resources/META-INF/frontend/fc-chat-assistant-resize.js @@ -0,0 +1,290 @@ +/*- + * #%L + * Chat Assistant Add-on + * %% + * Copyright (C) 2023 - 2026 Flowing Code + * %% + * 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. + * #L% + */ + +// Combined resize functionality for all directions +window.fcChatAssistantResize = (item, container, popoverTag, sizeRaw, maxSizeRaw, direction) => { + const size = parseFloat(sizeRaw); + const maxSize = parseFloat(maxSizeRaw); + const overlayTag = "vaadin-popover-overlay".toUpperCase(); + + let minWidth = 0; + let minHeight = 0; + let maxWidth = Infinity; + let maxHeight = Infinity; + let overlay; + let isDragging = false; + + const directionConfig = { + 'top': { + shouldDrag: () => overlay && overlay.style.bottom && !overlay.style.top, + handleResize: (e) => { + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.marginTop = ''; + } + }, + 'top-right': { + shouldDrag: () => { + const topRule = overlay && overlay.style.bottom && !overlay.style.top; + const rightRule = overlay && overlay.style.left && !overlay.style.right; + return topRule && rightRule; + }, + handleResize: (e) => { + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginRight = ''; + } + }, + 'right': { + shouldDrag: () => overlay && overlay.style.left && !overlay.style.right, + handleResize: (e) => { + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.width = maxSize + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.width = size + 'px'; + item.style.marginRight = ''; + } + }, + 'bottom-right': { + shouldDrag: () => { + const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; + const rightRule = overlay && overlay.style.left && !overlay.style.right; + return bottomRule && rightRule; + }, + handleResize: (e) => { + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = e.clientX - container.getBoundingClientRect().right; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + item.style.marginRight = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginRight = ''; + } + }, + 'bottom': { + shouldDrag: () => overlay && !overlay.style.bottom && overlay.style.top, + handleResize: (e) => { + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if (newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.marginBottom = ''; + } + }, + 'bottom-left': { + shouldDrag: () => { + const bottomRule = overlay && !overlay.style.bottom && overlay.style.top; + const leftRule = overlay && overlay.style.right && !overlay.style.left; + return bottomRule && leftRule; + }, + handleResize: (e) => { + const offsetY = e.clientY - container.getBoundingClientRect().bottom; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginBottom = -(maxSize / 2) + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginBottom = ''; + item.style.marginLeft = ''; + } + }, + 'left': { + shouldDrag: () => overlay && overlay.style.right && !overlay.style.left, + handleResize: (e) => { + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.width = maxSize + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.width = size + 'px'; + item.style.marginLeft = ''; + } + }, + 'top-left': { + shouldDrag: () => { + const topRule = overlay && overlay.style.bottom && !overlay.style.top; + const leftRule = overlay && overlay.style.right && !overlay.style.left; + return topRule && leftRule; + }, + handleResize: (e) => { + const offsetY = container.getBoundingClientRect().top - e.clientY; + const newHeight = offsetY + container.clientHeight; + if(newHeight >= minHeight && newHeight <= maxHeight) { + container.style.height = newHeight + 'px'; + } + const offsetX = container.getBoundingClientRect().left - e.clientX; + const newWidth = offsetX + container.clientWidth; + if (newWidth >= minWidth && newWidth <= maxWidth) { + container.style.width = newWidth + 'px'; + } + }, + setupDrag: () => { + item.style.height = maxSize + 'px'; + item.style.width = maxSize + 'px'; + item.style.marginTop = -(maxSize / 2) + 'px'; + item.style.marginLeft = -(maxSize / 2) + 'px'; + }, + cleanupDrag: () => { + item.style.height = size + 'px'; + item.style.width = size + 'px'; + item.style.marginTop = ''; + item.style.marginLeft = ''; + } + } + }; + + const config = directionConfig[direction]; + if (!config) { + console.error(`Invalid direction: ${direction}. Valid directions: ${Object.keys(directionConfig).join(', ')}`); + return; + } + + window.requestAnimationFrame(fetchOverlay); + setTimeout(fetchOverlay, 2000); // in case the overlay is not available immediately, check again after 2 seconds + + // Fetch the root overlay component + function fetchOverlay() { + if (!overlay) { + overlay = document.querySelector(`.${popoverTag}`)?.shadowRoot?.querySelector(overlayTag); + if(!overlay) { + overlay = [...document.getElementsByClassName(popoverTag)].find(p => p.tagName == overlayTag); + } + } + } + + item.addEventListener('pointerenter', (e) => { + if (config.shouldDrag()) { + item.classList.add('active'); + minHeight = container.style.minHeight ? parseFloat(container.style.minHeight) : 0; + minWidth = container.style.minWidth ? parseFloat(container.style.minWidth) : 0; + maxWidth = container.style.maxWidth ? parseFloat(container.style.maxWidth) : Infinity; + maxHeight = container.style.maxHeight ? parseFloat(container.style.maxHeight) : Infinity; + } + else { + item.classList.remove('active'); + } + }); + + item.addEventListener('pointerdown', (e) => { + isDragging = config.shouldDrag(); + if (isDragging) { + item.setPointerCapture(e.pointerId); + config.setupDrag(); + } + }); + + item.addEventListener('pointermove', (e) => { + if (!isDragging) return; + config.handleResize(e); + }); + + item.addEventListener('pointerup', (e) => stopDragging(e)); + item.addEventListener('pointerleave', (e) => stopDragging(e)); + item.addEventListener('pointercancel', (e) => stopDragging(e)); + + function stopDragging(e) { + isDragging = false; + item.classList.remove('active'); + config.cleanupDrag(); + if (item.hasPointerCapture(e.pointerId)) { + item.releasePointerCapture(e.pointerId); + } + } +};