diff --git a/README.md b/README.md
index 8c49dbe..93aebbc 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ This component is a wrapper of [Sharee](https://github.com/parsagholipour/sharee
- Default social media drivers to share links easily: Copy, Twitter, Facebook, Linkedin, Whatsapp, Telegram.
- Modes to display the Easy Share menu: Fixed, Normal, Hover, Text, Dropdown.
- By defaut, locale and keys are in English but custom locales and keys can be defined.
+- Listen to share actions to track which driver was clicked.
## Supported versions
@@ -114,6 +115,20 @@ NormalShareEasy.create().withNoTitle(true).forComponent(div);
add(div);
```
+## Listening to share actions
+
+Register a listener to be notified when one of the share drivers is clicked. The event exposes the
+clicked driver (both as the raw name and, for default drivers, as a `Driver` enum value) and the
+share link:
+
+```java
+Div div = new Div();
+NormalShareEasy.create()
+ .withShareListener(event -> Notification.show("Shared via " + event.getDriverName()))
+ .forComponent(div);
+add(div);
+```
+
## Special configuration when using Spring
By default, Vaadin Flow only includes ```com/vaadin/flow/component``` to be always scanned for UI components and views. For this reason, the addon might need to be whitelisted in order to display correctly.
diff --git a/pom.xml b/pom.xml
index 0264d3e..5df14f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.vaadin.addons.flowingcode
share-easy-addon
- 2.1.1-SNAPSHOT
+ 2.2.0-SNAPSHOT
Share Easy Add-on
Share Eady Add-on for Vaadin Flow
@@ -148,7 +148,7 @@
io.github.bonigarcia
webdrivermanager
- 5.1.1
+ 5.9.2
test
diff --git a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/BaseShareEasy.java b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/BaseShareEasy.java
index 51e0385..5d38941 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/BaseShareEasy.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/BaseShareEasy.java
@@ -31,6 +31,7 @@
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import elemental.json.JsonObject;
+import elemental.json.JsonString;
/**
* BaseSharee represents the base class for creating Sharee instances.
@@ -49,9 +50,11 @@ class BaseShareEasy> {
private Options options = new Options();
Map customDrivers = new HashMap<>();
-
+
protected boolean withDefaultDriversListFirst = true;
+ private ShareEasyClickListener shareListener;
+
/**
* Sets the share link for the shared content.
*
@@ -121,6 +124,17 @@ public T withDrivers(ShareEasyDriver... drivers) {
return (T) this;
}
+ /**
+ * Sets the listener that is notified when one of the share drivers is clicked.
+ *
+ * @param shareListener the listener to notify on driver clicks
+ * @return The current instance of the BaseSharee class
+ */
+ public T withShareListener(ShareEasyClickListener shareListener) {
+ this.shareListener = shareListener;
+ return (T) this;
+ }
+
/**
* Initializes the Sharee component with the specified Vaadin component.
*
@@ -141,6 +155,7 @@ protected JsonObject getJsonObjectOptions() {
}
private void createSharee() {
+ registerShareListener();
String shareeOptions = getJsonObjectOptions().toJson();
if(customDrivers.isEmpty()) {
this.create(shareeOptions);
@@ -162,6 +177,22 @@ private void create(String shareeOptions) {
component.getElement().executeJs("fcShareeConnector.create(this, $0)", shareeOptions);
}
+ private void registerShareListener() {
+ if (shareListener == null) {
+ return;
+ }
+ component.getElement().addEventListener("driver-clicked", event -> {
+ JsonObject detail = event.getEventData();
+ String driverName = getStringOrNull(detail, "event.detail.name");
+ String link = getStringOrNull(detail, "event.detail.link");
+ shareListener.onShare(new ShareEasyClickEvent(component, driverName, link));
+ }).addEventData("event.detail.name").addEventData("event.detail.link");
+ }
+
+ private static String getStringOrNull(JsonObject data, String key) {
+ return data.hasKey(key) && data.get(key) instanceof JsonString ? data.getString(key) : null;
+ }
+
/**
* Sets custom language keys for the default locale.
*
diff --git a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickEvent.java b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickEvent.java
new file mode 100644
index 0000000..a3bb648
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickEvent.java
@@ -0,0 +1,93 @@
+/*-
+ * #%L
+ * Share Easy 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%
+ */
+package com.flowingcode.vaadin.addons.shareeasy;
+
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.Optional;
+import com.flowingcode.vaadin.addons.shareeasy.enums.Driver;
+import com.vaadin.flow.component.Component;
+
+/**
+ * Event fired when one of the share drivers is clicked.
+ *
+ * @author Paola De Bartolo / Flowing Code
+ */
+@SuppressWarnings("serial")
+public class ShareEasyClickEvent implements Serializable {
+
+ private final Component source;
+
+ private final String driverName;
+
+ private final String link;
+
+ /**
+ * Creates a new share click event.
+ *
+ * @param source the component the Share Easy instance is attached to
+ * @param driverName the name of the clicked driver, e.g. "telegram"
+ * @param link the share link associated with the driver (the copied URL for the copy driver); may
+ * be {@code null} for custom drivers without a link
+ */
+ public ShareEasyClickEvent(Component source, String driverName, String link) {
+ this.source = Objects.requireNonNull(source, "source must not be null");
+ this.driverName = Objects.requireNonNull(driverName, "driverName must not be null");
+ this.link = link;
+ }
+
+ /**
+ * Gets the component the Share Easy instance is attached to.
+ *
+ * @return the source component
+ */
+ public Component getSource() {
+ return source;
+ }
+
+ /**
+ * Gets the name of the clicked driver.
+ *
+ * @return the driver name, e.g. "telegram"
+ */
+ public String getDriverName() {
+ return driverName;
+ }
+
+ /**
+ * Gets the clicked driver as a {@link Driver default driver}, if it corresponds to one.
+ *
+ * @return the matching default {@link Driver}, or an empty {@link Optional} if the clicked driver
+ * is a custom driver
+ */
+ public Optional getDriver() {
+ return Driver.fromName(driverName);
+ }
+
+ /**
+ * Gets the share link associated with the clicked driver. For the copy driver this is the URL that
+ * was copied to the clipboard.
+ *
+ * @return the share link, or {@code null} for custom drivers without a link
+ */
+ public String getLink() {
+ return link;
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickListener.java b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickListener.java
new file mode 100644
index 0000000..37ee20c
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/ShareEasyClickListener.java
@@ -0,0 +1,43 @@
+/*-
+ * #%L
+ * Share Easy 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%
+ */
+package com.flowingcode.vaadin.addons.shareeasy;
+
+import com.vaadin.flow.function.SerializableConsumer;
+
+/**
+ * Listener notified when one of the share drivers is clicked.
+ *
+ * @author Paola De Bartolo / Flowing Code
+ */
+@FunctionalInterface
+public interface ShareEasyClickListener extends SerializableConsumer {
+
+ /**
+ * Invoked when a share driver is clicked.
+ *
+ * @param event the share click event
+ */
+ void onShare(ShareEasyClickEvent event);
+
+ @Override
+ default void accept(ShareEasyClickEvent event) {
+ onShare(event);
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/enums/Driver.java b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/enums/Driver.java
index f9f6165..5140cb4 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/shareeasy/enums/Driver.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/shareeasy/enums/Driver.java
@@ -19,6 +19,7 @@
*/
package com.flowingcode.vaadin.addons.shareeasy.enums;
+import java.util.Optional;
import com.flowingcode.vaadin.addons.shareeasy.ShareEasyDriver;
/**
@@ -40,4 +41,21 @@ private Driver(String name) {
public String getName() {
return name;
}
+
+ /**
+ * Resolves a default driver from its name (the name carried by the share event), if it matches one
+ * of the default drivers.
+ *
+ * @param name the driver name, e.g. "telegram"
+ * @return the matching {@link Driver}, or an empty {@link Optional} if the name does not correspond
+ * to a default driver (for instance, a custom driver)
+ */
+ public static Optional fromName(String name) {
+ for (Driver driver : values()) {
+ if (driver.name.equals(name)) {
+ return Optional.of(driver);
+ }
+ }
+ return Optional.empty();
+ }
}
diff --git a/src/main/resources/META-INF/resources/frontend/src/fc-sharee-connector.js b/src/main/resources/META-INF/resources/frontend/src/fc-sharee-connector.js
index 05f26b5..8f2bd64 100644
--- a/src/main/resources/META-INF/resources/frontend/src/fc-sharee-connector.js
+++ b/src/main/resources/META-INF/resources/frontend/src/fc-sharee-connector.js
@@ -24,6 +24,7 @@ window.fcShareeConnector = {
create: function (container, optionsJson) {
this._updateXButtonLabel();
this._updateCopyDriverOnClick();
+ this._patchDriverNames();
let parsedOptions = JSON.parse(optionsJson);
const sharee = new Sharee(container, parsedOptions);
},
@@ -31,6 +32,7 @@ window.fcShareeConnector = {
createWithCustomDrivers: function (container, optionsJson) {
this._updateXButtonLabel();
this._updateCopyDriverOnClick();
+ this._patchDriverNames();
let parsedOptions = JSON.parse(optionsJson);
// add the custom drivers to the list of drivers
let drivers = parsedOptions.drivers.concat(container.customDrivers);
@@ -95,6 +97,26 @@ window.fcShareeConnector = {
*/
_updateCopyDriverOnClick() {
Sharee.drivers['copy'].prototype.onClick = function(e) {
+ // The copy driver copies a URL instead of navigating, so the shared
+ // "link" is the text that gets copied (the base CopyDriver's getLink()
+ // is undefined). Dispatch the same "driver-clicked" event the base
+ // driver emits via super.onClick (lost when onClick is fully replaced
+ // here), carrying the actual copied URL.
+ const copyText = this.options?.shareLink || window.location.href
+ const event = new CustomEvent('driver-clicked', {
+ detail: {
+ name: this.getName(),
+ link: copyText,
+ originalEvent: e,
+ },
+ bubbles: true,
+ composed: true,
+ });
+ this.options?.targetElement.dispatchEvent(event);
+ // Honor preventDefault as the base CopyDriver does after super.onClick.
+ if (e.defaultPrevented) {
+ return
+ }
const successText = this.lang['CopiedSuccessfully']
const el = e.currentTarget;
const textEl = el.querySelector('div:nth-child(2)')
@@ -102,7 +124,6 @@ window.fcShareeConnector = {
textEl.innerHTML = this.getButtonText()
return
}
- let copyText = this.options?.shareLink || window.location.href
navigator.clipboard.writeText(copyText).then(() => {
textEl.innerHTML = successText
textEl.style.transition = '300ms all'
@@ -125,5 +146,20 @@ window.fcShareeConnector = {
Sharee.drivers['x'].prototype.getButtonText = function () {
return "X";
}
- }
+ },
+
+ /**
+ * Overrides getName on each default driver so that it returns the driver's
+ * registered key (e.g. "telegram", "copy") instead of the bundled (and
+ * unstable) class name. This makes the name carried by the "driver-clicked"
+ * event reliable on the server side. Custom drivers already define their own
+ * getName in _addDriver, so they are not affected.
+ */
+ _patchDriverNames() {
+ Object.keys(Sharee.drivers).forEach(function (name) {
+ Sharee.drivers[name].prototype.getName = function () {
+ return name;
+ };
+ });
+ }
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/NormalModeDemo.java b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/NormalModeDemo.java
index 609642b..5557cae 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/NormalModeDemo.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/NormalModeDemo.java
@@ -27,6 +27,7 @@
import com.flowingcode.vaadin.addons.shareeasy.util.CustomDriverOptions;
import com.flowingcode.vaadin.addons.shareeasy.util.LanguageKeys;
import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
@@ -127,12 +128,26 @@ public NormalModeDemo() {
customDrivers.put("Trello", new TrelloDriverOptions());
NormalShareEasy.create().withCustomDrivers(customDrivers).forComponent(normalDiv7);
// #if vaadin eq 0
- Div example7 = createContainerDiv("With custom driver for extra social: Trello", normalDiv7);
+ Div example7 = createContainerDiv("With custom driver for extra social: Trello", normalDiv7);
SourceCodeViewer.highlightOnHover(example7, "example7");
add(example7);
+ addSeparator();
// #endif
// show-source add(normalDiv7);
// end-block
+
+ // begin-block example8
+ Div normalDiv8 = new Div();
+ NormalShareEasy.create()
+ .withShareListener(event -> Notification.show("Shared via " + event.getDriverName()))
+ .forComponent(normalDiv8);
+ // #if vaadin eq 0
+ Div example8 = createContainerDiv("With share listener", normalDiv8);
+ SourceCodeViewer.highlightOnHover(example8, "example8");
+ add(example8);
+ // #endif
+ // show-source add(normalDiv8);
+ // end-block
}
}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyElement.java b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyElement.java
index e74e794..275d7aa 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyElement.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyElement.java
@@ -30,27 +30,32 @@ public List getAllDriversAnchors() {
}
public boolean copyExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__F").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__copy").exists();
}
public boolean telegramExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__I").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__telegram")
+ .exists();
}
public boolean facebookExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__G").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__facebook")
+ .exists();
}
public boolean whatsappExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__P").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__whatsapp")
+ .exists();
}
public boolean twitterExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__W").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__twitter")
+ .exists();
}
public boolean linkedinExists() {
- return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__A").exists();
+ return this.$(AnchorElement.class).attributeContains("class", "sharee__driver__linkedin")
+ .exists();
}
public boolean normalModeContainAllDefaultValues() {
diff --git a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyNormalModeIT.java b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyNormalModeIT.java
index ac62fdb..64fc813 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyNormalModeIT.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/shareeasy/it/ShareEasyNormalModeIT.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.vaadin.flow.component.html.testbench.AnchorElement;
+import com.vaadin.flow.component.notification.testbench.NotificationElement;
public class ShareEasyNormalModeIT extends BaseShareEasyIT {
@@ -72,4 +73,20 @@ public void withExtraSocial() {
assertTrue("Normal Share Easy shows no extra social",
driversCount == DEFAULT_NORMAL_DRIVER_COUNT + 1);
}
+
+ @Test
+ public void withShareListener_clickDriver_firesEvent() {
+ // The Sharee instances are created asynchronously via executeJs, so wait until the
+ // share-listener example (the 8th normal share) has rendered before interacting.
+ waitUntil(driver -> $(ShareEasyElement.class).attributeContains("class", "sharee__normal").all()
+ .size() > 7);
+ ShareEasyElement normalWithShareListener =
+ $(ShareEasyElement.class).attributeContains("class", "sharee__normal").get(7);
+ AnchorElement copyDriver = normalWithShareListener.$(AnchorElement.class)
+ .attributeContains("class", "sharee__driver__copy").waitForFirst();
+ copyDriver.click();
+ NotificationElement notification = $(NotificationElement.class).waitForFirst();
+ assertTrue("Share listener was not notified with the clicked driver name",
+ notification.getText().contains("copy"));
+ }
}