Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting RichSuggestBox #65

Merged
merged 7 commits into from
May 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components/RichSuggestBox/OpenSolution.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@ECHO OFF

powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
31 changes: 31 additions & 0 deletions components/RichSuggestBox/samples/Dependencies.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!--
WinUI 2 under UWP uses TargetFramework uap10.0.*
WinUI 3 under WinAppSdk uses TargetFramework net6.0-windows10.*
However, under Uno-powered platforms, both WinUI 2 and 3 can share the same TargetFramework.

MSBuild doesn't play nicely with this out of the box, so we've made it easy for you.

For .NET Standard packages, you can use the Nuget Package Manager in Visual Studio.
For UWP / WinAppSDK / Uno packages, place the package references here.
-->
<Project>
<!-- WinUI 2 / UWP -->
<ItemGroup Condition="'$(IsUwp)' == 'true'">
<!-- <PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 2 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '2'">
<!-- <PackageReference Include="Uno.Microsoft.Toolkit.Uwp.UI.Controls.Primitives" Version="7.1.11"/> -->
</ItemGroup>

<!-- WinUI 3 / WinAppSdk -->
<ItemGroup Condition="'$(IsWinAppSdk)' == 'true'">
<!-- <PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.2"/> -->
</ItemGroup>

<!-- WinUI 3 / Uno -->
<ItemGroup Condition="'$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3'">
<!-- <PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls.Primitives" Version="7.1.100-dev.15.g12261e2626"/> -->
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="MSBuild.Sdk.Extras/3.0.23">
<PropertyGroup>
<ToolkitComponentName>RichSuggestBox</ToolkitComponentName>
</PropertyGroup>

<!-- Sets this up as a toolkit component's sample project -->
<Import Project="$(ToolingDirectory)\ToolkitComponent.SampleProject.props" />
</Project>
102 changes: 102 additions & 0 deletions components/RichSuggestBox/samples/RichSuggestBox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: RichSuggestBox
author: huynhsontung
description: A rich text input control that auto-suggests and stores token items in a document.
keywords: RichSuggestBox, Control,
dev_langs:
- csharp
category: Controls
subcategory: Input
discussion-id: 0
issue-id: 0
---

# RichSuggestBox

The [RichSuggestBox](/dotnet/api/microsoft.toolkit.uwp.ui.controls.richsuggestbox) is a combination of [AutoSuggestBox](/uwp/api/windows.ui.xaml.controls.autosuggestbox) and [RichEditBox](/uwp/api/windows.ui.xaml.controls.richeditbox) that can provide suggestions based on customizable prefixes. Selected suggestions are embedded and tracked in the document as tokens.

RichSuggestBox resembles text controls commonly found in social applications where you type "@" to mention people.

> **Platform APIs:** [`RichSuggestBox`](/dotnet/api/microsoft.toolkit.uwp.ui.controls.richsuggestbox)

> [!Sample RichSuggestBoxMultiplePrefixesSample]

## Syntax

```xaml
<controls:RichSuggestBox
PlaceholderText="Leave a comment"
ItemTemplate="{StaticResource SuggestionTemplate}"
Prefixes="@#" />
```

## Remarks

When a suggestion is selected, `RichSuggestBox` assigns the selected item a unique [Guid](/dotnet/api/system.guid) and a display text (provided by the developer) to make up a token. The display text is then padded with [Zero Width Space](https://unicode-table.com/200B/)s (ZWSP) and inserted into the document as a hyperlink using the identifier as the link address. These hyperlinks are tracked and validated on every text change.

The token text inserted into the document has the following layout: ZWSP - Prefix character - Display text - ZWSP.

For example, a token with "@" as the prefix and "John Doe" as the display text is inserted as:

```cs
"\u200b@John Doe\u200b"
```

> [!IMPORTANT]
> Token text contains [Zero Width Space](https://unicode-table.com/200B/)s, which are Unicode characters.

> [!NOTE]
> To support Undo/Redo function, `RichSuggestBox` keeps all the tokens in an internal collection even when the token text is deleted from the document. These token are marked as inactive and are not included in the `Tokens` collection. Use `ClearUndoRedoSuggestionHistory()` method to clear inactive tokens or `Clear()` method to clear all tokens.

## Examples

### Handle multiple token types

The example below creates a `RichSuggestBox` that can tokenize both mentions (query starts with `@`) and hashtags (query starts with `#`).

```xaml
<controls:RichSuggestBox
PlaceholderText="Leave a comment"
ItemTemplate="{StaticResource SuggestionTemplate}"
SuggestionChosen="OnSuggestionChosen"
SuggestionRequested="OnSuggestionRequested"
Prefixes="@#" />
```

```cs
private void OnSuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
if (args.Prefix == "#")
{
// User selected a hashtag item
args.DisplayText = ((SampleHashtagDataType)args.SelectedItem).Text;
}
else
{
// User selected a mention item
args.DisplayText = ((SampleEmailDataType)args.SelectedItem).DisplayName;
}
}

private void OnSuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
sender.ItemsSource = args.Prefix == "#"
? _hashtags.Where(x => x.Text.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase))
: _emails.Where(x => x.DisplayName.Contains(args.QueryText, StringComparison.OrdinalIgnoreCase));
}
```

### Plain text only

The example below creates a `RichSuggestBox` that only allows users to enter plain text. The only formatted texts in the document are tokens.

```xaml
<controls:RichSuggestBox
ClipboardCopyFormat="PlainText"
ClipboardPasteFormat="PlainText"
DisabledFormattingAccelerators="All" />
```

> [!Sample RichSuggestBoxPlainTextSample]


Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
<Page x:Class="RichSuggestBoxExperiment.Samples.RichSuggestBoxMultiplePrefixesSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:RichSuggestBoxExperiment.Samples"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="EmailTemplate">
<StackPanel Orientation="Horizontal">
<Border Width="20"
Height="20"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="9999">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontWeight="Semibold"
Foreground="White"
Text="{Binding Initials}" />
</Border>
<TextBlock Padding="8,0,0,0"
Text="{Binding DisplayName}" />
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="DataTemplate">
<StackPanel Orientation="Horizontal">
<SymbolIcon Symbol="{Binding Icon}" />
<TextBlock Margin="8,0,0,0"
Text="{Binding Text}" />
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="TokenTemplate">
<StackPanel Margin="0,4,0,12"
Orientation="Vertical">
<TextBlock>
<Run Text="Text:" />
<Run FontWeight="SemiBold"
Text="{Binding DisplayText}" />
</TextBlock>
<TextBlock>
<Run Text="Position:" />
<Run FontWeight="SemiBold"
Text="{Binding Position}" />
</TextBlock>

<TextBlock>
<Run Text="Id:" />
<Run FontWeight="SemiBold"
Text="{Binding Id}" />
</TextBlock>
</StackPanel>
</DataTemplate>

<local:SuggestionTemplateSelector x:Key="SuggestionTemplateSelector"
Data="{StaticResource DataTemplate}"
Person="{StaticResource EmailTemplate}" />

</Page.Resources>
<StackPanel MinWidth="400"
HorizontalAlignment="Center"
Spacing="24">
<controls:RichSuggestBox x:Name="SuggestingBox"
HorizontalAlignment="Stretch"
Header="RichSuggestBox that supports multiple prefixes (@ and #)"
ItemTemplateSelector="{StaticResource SuggestionTemplateSelector}"
Prefixes="@#"
SuggestionChosen="SuggestingBox_SuggestionChosen"
SuggestionRequested="SuggestingBox_SuggestionRequested" />
<ListView x:Name="TokenListView"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource TokenTemplate}" />
</StackPanel>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.WinUI.Controls;

#if WINAPPSDK
using Microsoft.UI;
using Microsoft.UI.Text;
#else
using Windows.UI;
using Windows.UI.Text;
#endif

namespace RichSuggestBoxExperiment.Samples;

[ToolkitSample(id: nameof(RichSuggestBoxMultiplePrefixesSample), "RichSuggestBox with multiple prefixes", description: $"A sample for showing how to create and use a {nameof(RichSuggestBox)} with multiple prefixes.")]
public sealed partial class RichSuggestBoxMultiplePrefixesSample : Page
{
private readonly List<SampleEmailDataType> _emailSamples = new List<SampleEmailDataType>()
{
new SampleEmailDataType() { FirstName = "Marcus", FamilyName = "Perryman" },
new SampleEmailDataType() { FirstName = "Michael", FamilyName = "Hawker" },
new SampleEmailDataType() { FirstName = "Matt", FamilyName = "Lacey" },
new SampleEmailDataType() { FirstName = "Alexandre", FamilyName = "Chohfi" },
new SampleEmailDataType() { FirstName = "Filip", FamilyName = "Wallberg" },
new SampleEmailDataType() { FirstName = "Shane", FamilyName = "Weaver" },
new SampleEmailDataType() { FirstName = "Vincent", FamilyName = "Gromfeld" },
new SampleEmailDataType() { FirstName = "Sergio", FamilyName = "Pedri" },
new SampleEmailDataType() { FirstName = "Alex", FamilyName = "Wilber" },
new SampleEmailDataType() { FirstName = "Allan", FamilyName = "Deyoung" },
new SampleEmailDataType() { FirstName = "Adele", FamilyName = "Vance" },
new SampleEmailDataType() { FirstName = "Grady", FamilyName = "Archie" },
new SampleEmailDataType() { FirstName = "Megan", FamilyName = "Bowen" },
new SampleEmailDataType() { FirstName = "Ben", FamilyName = "Walters" },
new SampleEmailDataType() { FirstName = "Debra", FamilyName = "Berger" },
new SampleEmailDataType() { FirstName = "Emily", FamilyName = "Braun" },
new SampleEmailDataType() { FirstName = "Christine", FamilyName = "Cline" },
new SampleEmailDataType() { FirstName = "Enrico", FamilyName = "Catteneo" },
new SampleEmailDataType() { FirstName = "Davit", FamilyName = "Badalyan" },
new SampleEmailDataType() { FirstName = "Diego", FamilyName = "Siciliani" },
new SampleEmailDataType() { FirstName = "Raul", FamilyName = "Razo" },
new SampleEmailDataType() { FirstName = "Miriam", FamilyName = "Graham" },
new SampleEmailDataType() { FirstName = "Lynne", FamilyName = "Robbins" },
new SampleEmailDataType() { FirstName = "Lydia", FamilyName = "Holloway" },
new SampleEmailDataType() { FirstName = "Nestor", FamilyName = "Wilke" },
new SampleEmailDataType() { FirstName = "Patti", FamilyName = "Fernandez" },
new SampleEmailDataType() { FirstName = "Pradeep", FamilyName = "Gupta" },
new SampleEmailDataType() { FirstName = "Joni", FamilyName = "Sherman" },
new SampleEmailDataType() { FirstName = "Isaiah", FamilyName = "Langer" },
new SampleEmailDataType() { FirstName = "Irvin", FamilyName = "Sayers" },
new SampleEmailDataType() { FirstName = "Tung", FamilyName = "Huynh" },
};

private readonly List<SampleDataType> _samples = new List<SampleDataType>()
{
new SampleDataType() { Text = "Account", Icon = Symbol.Account },
new SampleDataType() { Text = "Add Friend", Icon = Symbol.AddFriend },
new SampleDataType() { Text = "Attach", Icon = Symbol.Attach },
new SampleDataType() { Text = "Attach Camera", Icon = Symbol.AttachCamera },
new SampleDataType() { Text = "Audio", Icon = Symbol.Audio },
new SampleDataType() { Text = "Block Contact", Icon = Symbol.BlockContact },
new SampleDataType() { Text = "Calculator", Icon = Symbol.Calculator },
new SampleDataType() { Text = "Calendar", Icon = Symbol.Calendar },
new SampleDataType() { Text = "Camera", Icon = Symbol.Camera },
new SampleDataType() { Text = "Contact", Icon = Symbol.Contact },
new SampleDataType() { Text = "Favorite", Icon = Symbol.Favorite },
new SampleDataType() { Text = "Link", Icon = Symbol.Link },
new SampleDataType() { Text = "Mail", Icon = Symbol.Mail },
new SampleDataType() { Text = "Map", Icon = Symbol.Map },
new SampleDataType() { Text = "Phone", Icon = Symbol.Phone },
new SampleDataType() { Text = "Pin", Icon = Symbol.Pin },
new SampleDataType() { Text = "Rotate", Icon = Symbol.Rotate },
new SampleDataType() { Text = "Rotate Camera", Icon = Symbol.RotateCamera },
new SampleDataType() { Text = "Send", Icon = Symbol.Send },
new SampleDataType() { Text = "Tags", Icon = Symbol.Tag },
new SampleDataType() { Text = "UnFavorite", Icon = Symbol.UnFavorite },
new SampleDataType() { Text = "UnPin", Icon = Symbol.UnPin },
new SampleDataType() { Text = "Zoom", Icon = Symbol.Zoom },
new SampleDataType() { Text = "ZoomIn", Icon = Symbol.ZoomIn },
new SampleDataType() { Text = "ZoomOut", Icon = Symbol.ZoomOut },
};
public RichSuggestBoxMultiplePrefixesSample()
{
this.InitializeComponent();
TokenListView.ItemsSource = SuggestingBox.Tokens;
}

private void SuggestingBox_SuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)
{
if (args.Prefix == "#")
{
args.Format!.BackgroundColor = Colors.LightSlateGray;
args.Format.ForegroundColor = Colors.White;
args.Format.Bold = FormatEffect.On;
args.DisplayText = ((SampleDataType)args.SelectedItem!).Text;
}
else
{
args.DisplayText = ((SampleEmailDataType)args.SelectedItem!).DisplayName;
}
}

private void SuggestingBox_SuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)
{
if (args.Prefix == "#")
{
sender.ItemsSource = this._samples.Where(x => x.Text.Contains(args.QueryText!, StringComparison.OrdinalIgnoreCase));
}
else
{
sender.ItemsSource = this._emailSamples.Where(x => x.DisplayName.Contains(args.QueryText!, StringComparison.OrdinalIgnoreCase));
}
}
}
Loading