diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs
index 6721adcc8..dc2f184bf 100644
--- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs
+++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs
@@ -244,7 +244,29 @@ internal bool OnPopupAttachmentClicked(PopupAttachment attachment)
if (handler is not null)
{
var args = new PopupAttachmentClickedEventArgs(attachment);
- PopupAttachmentClicked?.Invoke(this, args);
+ handler.Invoke(this, args);
+ return args.Handled;
+ }
+ return false;
+ }
+
+ ///
+ /// Raised when a link is clicked
+ ///
+ ///
+ /// By default, when an link is clicked, the default application (Browser) for the file type (if any) is launched. To override this,
+ /// listen to this event, set the property to true and perform
+ /// your own logic.
+ ///
+ public event EventHandler? HyperLinkClicked;
+
+ internal bool OnHyperLinkClicked(Uri uri)
+ {
+ var handler = HyperLinkClicked;
+ if (handler is not null)
+ {
+ var args = new HyperLinkClickedEventArgs(uri);
+ handler.Invoke(this, args);
return args.Handled;
}
return false;
@@ -271,5 +293,26 @@ internal PopupAttachmentClickedEventArgs(PopupAttachment attachment)
///
public PopupAttachment Attachment { get; }
}
+
+ ///
+ /// Event argument for the event.
+ ///
+ public class HyperLinkClickedEventArgs : EventArgs
+ {
+ internal HyperLinkClickedEventArgs(Uri uri)
+ {
+ Uri = uri;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the event handler has handled the event and the default action should be prevented.
+ ///
+ public bool Handled { get; set; }
+
+ ///
+ /// Gets the URI that was clicked.
+ ///
+ public Uri Uri { get; }
+ }
}
#endif
\ No newline at end of file
diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs
index 6f6db3bde..6643bdb6d 100644
--- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs
+++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Maui.cs
@@ -83,7 +83,7 @@ private void OnElementPropertyChanged()
}
}
- private static IEnumerable VisitChildren(MarkupNode parent)
+ private IEnumerable VisitChildren(MarkupNode parent)
{
// Create views for all the children of a given node.
// Nodes with blocks are converted individually, but consecutive inline-only nodes are grouped into labels.
@@ -116,7 +116,7 @@ private static IEnumerable VisitChildren(MarkupNode parent)
}
}
- private static View CreateBlock(MarkupNode node)
+ private View CreateBlock(MarkupNode node)
{
// Create a view for a single block node.
switch (node.Type)
@@ -218,7 +218,7 @@ static View VerticallyAlignTableCell(MarkupNode node, View cellContent)
}
}
- private static Label CreateFormattedText(IEnumerable nodes)
+ private Label CreateFormattedText(IEnumerable nodes)
{
// Flattens given tree of inline nodes into a single label.
var str = new FormattedString();
@@ -232,7 +232,7 @@ private static Label CreateFormattedText(IEnumerable nodes)
return new Label { FormattedText = str, LineBreakMode = LineBreakMode.WordWrap };
}
- private static IEnumerable VisitInline(MarkupNode node)
+ private IEnumerable VisitInline(MarkupNode node)
{
// Converts a single inline node into a sequence of spans.
// The whole tree is expected to only contain inline nodes. Other nodes are handled by VisitBlock.
@@ -245,11 +245,7 @@ private static IEnumerable VisitInline(MarkupNode node)
var tapRecognizer = new TapGestureRecognizer();
tapRecognizer.Tapped += (s, e) =>
{
- try
- {
- Browser.OpenAsync(node.Content, BrowserLaunchMode.SystemPreferred);
- }
- catch { }
+ OnHyperlinkClicked(linkUri);
};
foreach (var subNode in node.Children)
{
@@ -298,7 +294,7 @@ private static IEnumerable VisitInline(MarkupNode node)
}
}
- private static Grid ConvertTableToGrid(MarkupNode table)
+ private Grid ConvertTableToGrid(MarkupNode table)
{
// Determines the dimensions of a grid necessary to hold a given table.
// Utilizes a dynamically-sized 2D bitmap (`gridMap`) to mark occupied cells while iterating over the table.
@@ -455,6 +451,16 @@ private static bool MapsToBlock(MarkupNode node)
{
return node.Type is MarkupType.List or MarkupType.Table or MarkupType.Block or MarkupType.Divider or MarkupType.Image;
}
+
+ private PopupViewer? GetPopupViewerParent()
+ {
+ var parent = this.Parent;
+ while (parent is not null && parent is not PopupViewer popup)
+ {
+ parent = parent.Parent;
+ }
+ return parent as PopupViewer;
+ }
}
}
#endif
\ No newline at end of file
diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs
index b9a0956ac..9f12cb4f5 100644
--- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs
+++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs
@@ -17,6 +17,7 @@
#if WPF
using Esri.ArcGISRuntime.Mapping.Popups;
using Esri.ArcGISRuntime.Toolkit.Internal;
+using Esri.ArcGISRuntime.Toolkit.UI.Controls;
using Esri.ArcGISRuntime.UI;
using System.Windows.Documents;
using System.Windows.Input;
@@ -57,7 +58,7 @@ private void OnElementPropertyChanged()
}
}
- private static IEnumerable VisitAndAddBlocks(IEnumerable nodes)
+ private IEnumerable VisitAndAddBlocks(IEnumerable nodes)
{
Paragraph? inlineHolder = null;
foreach (var node in nodes)
@@ -81,7 +82,7 @@ private static IEnumerable VisitAndAddBlocks(IEnumerable node
yield return inlineHolder;
}
- private static IEnumerable VisitAndAddInlines(IEnumerable nodes)
+ private IEnumerable VisitAndAddInlines(IEnumerable nodes)
{
foreach (var node in nodes)
{
@@ -96,7 +97,7 @@ private static IEnumerable VisitAndAddInlines(IEnumerable no
}
}
- private static Block VisitBlock(MarkupNode node)
+ private Block VisitBlock(MarkupNode node)
{
switch (node.Type)
{
@@ -176,7 +177,7 @@ private static Block VisitBlock(MarkupNode node)
}
}
- private static Inline VisitInline(MarkupNode node)
+ private Inline VisitInline(MarkupNode node)
{
switch (node.Type)
{
@@ -287,9 +288,19 @@ private static System.Windows.Media.Color ConvertColor(System.Drawing.Color colo
_ => TextAlignment.Left,
};
- private static async void NavigateToUri(object sender, RequestNavigateEventArgs ea)
+ private void NavigateToUri(object sender, RequestNavigateEventArgs ea)
{
- await Launcher.LaunchUriAsync(ea.Uri);
+ OnHyperlinkClicked(ea.Uri);
+ }
+
+ private PopupViewer? GetPopupViewerParent()
+ {
+ var parent = VisualTreeHelper.GetParent(this);
+ while (parent is not null and not PopupViewer)
+ {
+ parent = VisualTreeHelper.GetParent(parent);
+ }
+ return parent as PopupViewer;
}
}
}
diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs
index 6cbb0409e..8e086efd6 100644
--- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs
+++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs
@@ -19,9 +19,13 @@
using Esri.ArcGISRuntime.Toolkit.Internal;
using Esri.ArcGISRuntime.UI;
#if WPF
+using Esri.ArcGISRuntime.Toolkit.UI.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Navigation;
+#elif MAUI
+using Esri.ArcGISRuntime.Toolkit.Maui;
+using Microsoft.Maui.ApplicationModel;
#endif
@@ -70,6 +74,41 @@ public TextPopupElement? Element
///
public static readonly DependencyProperty ElementProperty =
PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.OnElementPropertyChanged());
+
+ ///
+ /// Occurs when an URL is clicked.
+ ///
+ ///
+ /// Override this to prevent the default open action.
+ ///
+ /// Clicked URL
+ public virtual async void OnHyperlinkClicked(Uri uri)
+ {
+ if (uri is not null)
+ {
+ var viewer = GetPopupViewerParent();
+ if (viewer is not null)
+ {
+ var handled = viewer.OnHyperLinkClicked(uri);
+ if (handled)
+ return;
+ }
+
+ try
+ {
+#if WPF
+ await Launcher.LaunchUriAsync(uri);
+#elif MAUI
+ await Browser.OpenAsync(uri, BrowserLaunchMode.SystemPreferred);
+#endif
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Trace.WriteLine($"Failed to open URL: " + ex.Message);
+ }
+ }
+
+ }
}
}
#endif
\ No newline at end of file