Skip to content

Commit 59fdd4e

Browse files
authored
popup: Support clickable image links on MAUI, WinUI, and UWP (#682)
1 parent 2e00548 commit 59fdd4e

File tree

2 files changed

+101
-7
lines changed

2 files changed

+101
-7
lines changed

src/Toolkit/Toolkit.Maui/Internal/HtmlToView.Maui.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ internal static IEnumerable<View> VisitChildren(MarkupNode parent, EventHandler<
8282
}
8383
}
8484

85+
// CreateBlock builds a view for a single *blocky* node. It is called for nodes that are
86+
// blocks themselves (List/Table/Block/Divider/Image) and also for nodes that contain block descendants.
8587
private static View CreateBlock(MarkupNode node, EventHandler<Uri>? urlClickHandler)
8688
{
8789
// Create a view for a single block node.
@@ -161,6 +163,47 @@ private static View CreateBlock(MarkupNode node, EventHandler<Uri>? urlClickHand
161163
imageElement.Source = imageSource;
162164
return imageElement;
163165

166+
case MarkupType.Link:
167+
// If the link wraps block content (like <img>), render it as a tappable ContentView.
168+
if (Uri.TryCreate(node.Content, UriKind.Absolute, out var linkUri))
169+
{
170+
View content;
171+
if (node.Children.Count == 1)
172+
{
173+
var child = node.Children[0];
174+
child.InheritAttributes(node);
175+
content = CreateBlock(child, urlClickHandler);
176+
}
177+
else
178+
{
179+
var stack = new StackLayout();
180+
foreach (var child in node.Children)
181+
{
182+
child.InheritAttributes(node);
183+
stack.Children.Add(CreateBlock(child, urlClickHandler));
184+
}
185+
content = stack;
186+
}
187+
188+
var wrapper = new ContentView { Content = content, Padding = 0 };
189+
if (urlClickHandler != null)
190+
{
191+
var tap = new TapGestureRecognizer();
192+
tap.Tapped += (s, e) => urlClickHandler(wrapper, linkUri);
193+
wrapper.GestureRecognizers.Add(tap);
194+
}
195+
return wrapper;
196+
}
197+
198+
// If the href isn't a clickable URI, just render children normally
199+
var fallback = new StackLayout();
200+
foreach (var child in node.Children)
201+
{
202+
child.InheritAttributes(node);
203+
fallback.Children.Add(CreateBlock(child, urlClickHandler));
204+
}
205+
return fallback;
206+
164207
default:
165208
return new Border(); // placeholder for unsupported things
166209
}
@@ -198,6 +241,8 @@ private static Label CreateFormattedText(IEnumerable<MarkupNode> nodes, EventHan
198241
return new Label { FormattedText = str, LineBreakMode = LineBreakMode.WordWrap };
199242
}
200243

244+
// Flattens an *inline-only* subtree into text spans for a single Label.
245+
// It is only used when VisitChildren has verified there are no block elements underneath.
201246
private static IEnumerable<Span> VisitInline(MarkupNode node, EventHandler<Uri>? urlClickHandler)
202247
{
203248
// Converts a single inline node into a sequence of spans.
@@ -309,7 +354,7 @@ private static Grid ConvertTableToGrid(MarkupNode table, EventHandler<Uri>? urlC
309354
if (attr.TryGetValue("rowspan", out var rowSpanStr) && ushort.TryParse(rowSpanStr, out var rowSpanFromAttr))
310355
{
311356
rowSpan = rowSpanFromAttr;
312-
Grid.SetRowSpan(cellView, colSpan);
357+
Grid.SetRowSpan(cellView, rowSpan);
313358
}
314359
gridView.Add(cellView, curCol, curRow);
315360

src/Toolkit/Toolkit.WinUI/Internal/HtmlToView.WinUI.cs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ internal static IEnumerable<UIElement> VisitChildren(MarkupNode parent, EventHan
8484
}
8585
}
8686

87+
// CreateBlock builds a view for a single *blocky* node. It is called for nodes that are
88+
// blocks themselves (List/Table/Block/Divider/Image) and also for nodes that contain block descendants.
8789
private static FrameworkElement CreateBlock(MarkupNode node, EventHandler<Uri>? urlClickHandler)
8890
{
8991
// Create a view for a single block node.
@@ -167,6 +169,51 @@ private static FrameworkElement CreateBlock(MarkupNode node, EventHandler<Uri>?
167169
imageElement.Source = imageSource;
168170
return imageElement;
169171

172+
case MarkupType.Link:
173+
// If the link wraps block content (like <img>), render it as a HyperlinkButton
174+
if (Uri.TryCreate(node.Content, UriKind.Absolute, out var linkUri))
175+
{
176+
var linkButton = new HyperlinkButton
177+
{
178+
Padding = new Thickness(0),
179+
BorderThickness = new Thickness(0),
180+
Background = null
181+
};
182+
if (urlClickHandler != null)
183+
linkButton.Click += (s, e) => urlClickHandler(s, linkUri);
184+
185+
// Build the content from the children as blocks
186+
FrameworkElement content;
187+
if (node.Children.Count == 1)
188+
{
189+
var child = node.Children[0];
190+
child.InheritAttributes(node);
191+
content = CreateBlock(child, urlClickHandler);
192+
}
193+
else
194+
{
195+
var panel = new StackPanel();
196+
foreach (var child in node.Children)
197+
{
198+
child.InheritAttributes(node);
199+
panel.Children.Add(CreateBlock(child, urlClickHandler));
200+
}
201+
content = panel;
202+
}
203+
204+
linkButton.Content = content;
205+
return linkButton;
206+
}
207+
208+
// If the href isn't a clickable URI, just render children normally
209+
var fallback = new StackPanel();
210+
foreach (var child in node.Children)
211+
{
212+
child.InheritAttributes(node);
213+
fallback.Children.Add(CreateBlock(child, urlClickHandler));
214+
}
215+
return fallback;
216+
170217
default:
171218
return new Border(); // placeholder for unsupported things
172219
}
@@ -205,6 +252,8 @@ private static TextBlock CreateFormattedText(IEnumerable<MarkupNode> nodes, Even
205252
return tb;
206253
}
207254

255+
// Flattens an *inline-only* subtree into text spans for a single TextBlock.
256+
// It is only used when VisitChildren has verified there are no block elements underneath.
208257
private static IEnumerable<Span> VisitInline(MarkupNode node, EventHandler<Uri>? urlClickHandler)
209258
{
210259
// Converts a single inline node into a sequence of spans.
@@ -308,7 +357,7 @@ private static Grid ConvertTableToGrid(MarkupNode table, EventHandler<Uri>? urlC
308357
if (attr.TryGetValue("rowspan", out var rowSpanStr) && ushort.TryParse(rowSpanStr, out var rowSpanFromAttr))
309358
{
310359
rowSpan = rowSpanFromAttr;
311-
Grid.SetRowSpan(cellView, colSpan);
360+
Grid.SetRowSpan(cellView, rowSpan);
312361
}
313362
Grid.SetRow(cellView, curRow);
314363
Grid.SetColumn(cellView, curCol);
@@ -358,9 +407,9 @@ private static void ApplyStyle(Span el, MarkupNode node)
358407
el.FontWeight = FontWeights.Normal;
359408

360409
if (node.IsItalic == true)
361-
el.FontStyle |= FontStyle.Italic;
410+
el.FontStyle = FontStyle.Italic;
362411
else if (node.IsItalic == false)
363-
el.TextDecorations |= ~TextDecorations.Underline;
412+
el.FontStyle = FontStyle.Normal;
364413

365414
if (node.IsUnderline == true)
366415
el.TextDecorations |= TextDecorations.Underline;
@@ -386,14 +435,14 @@ private static void ApplyStyle(TextBlock el, MarkupNode node)
386435
el.FontWeight = FontWeights.Normal;
387436

388437
if (node.IsItalic == true)
389-
el.FontStyle |= FontStyle.Italic;
438+
el.FontStyle = FontStyle.Italic;
390439
else if (node.IsItalic == false)
391-
el.FontStyle |= ~FontStyle.Italic;
440+
el.FontStyle = FontStyle.Normal;
392441

393442
if (node.IsUnderline == true)
394443
el.TextDecorations |= TextDecorations.Underline;
395444
else if (node.IsUnderline == false)
396-
el.TextDecorations |= ~TextDecorations.Underline;
445+
el.TextDecorations &= ~TextDecorations.Underline;
397446

398447
if (node.FontColor.HasValue)
399448
el.Foreground = new SolidColorBrush() { Color = ConvertColor(node.FontColor.Value) };

0 commit comments

Comments
 (0)