Blazored / Typeahead

Typeahead control for Blazor applications
https://blazored.github.io/Typeahead/
MIT License
433 stars 103 forks source link

Allow distinct styling of matching characters #82

Open chucker opened 4 years ago

chucker commented 4 years ago

Right now, the entire result is placed in a div. Given the example template:

        <ResultTemplate>
            @context.Title (@context.Year)
        </ResultTemplate>

, a result div might be:

<div class="blazored-typeahead__result ">
  "The Shining"
  "(1977)"
</div>

If I searched for Shi, I would like the following instead:

<div class="blazored-typeahead__result ">
  "The "
  <span class="blazored-typeahead__result_match">
    "Shi"
  </span>
  "ning"
  "(1977)"
</div>

I could then supply styling information for that class and make matching characters bold, underline, or similar:

.blazored-typeahead__result_match
{
    font-weight: bold
}

Similar Stack Overflow discussion

vertonghenb commented 4 years ago

I like the idea, doesn't have a lot of code to it. @chucker want to PR this one? We might want to add a parameter that controls this behavior though. HighlightSearchInSuggestions or something similar.

chrissainty commented 4 years ago

The interesting challenge here is how we highlight text in the template as we don't have access to it until its rendered. I think this is only going to be possible using JS interop.

vertonghenb commented 4 years ago

@chrissainty true! Since we don't have control over de template. a workaround for this would be to provide the functionality yourself since the user has access to the template.

vertonghenb commented 4 years ago

I was thinking about this today and I believe we could give the control to the user but that would involve exposing the SearchTerm to the caller. Since he will need the SearchTerm to highlight the part of the template. Maybe I'm overthinking this, but it would be nice if we could come up with some idea how to tackle this problem.

chucker commented 4 years ago

The interesting challenge here is how we highlight text in the template as we don't have access to it until its rendered.

Hm, seems the other problem is that we don't actually have the matching characters, since the matching function (SearchMethod) is something provided by the consumer.

erinnmclaughlin commented 4 years ago

I was thinking about this today and I believe we could give the control to the user but that would involve exposing the SearchTerm to the caller.

If this was just exposed via public getter, private setter -- would there be any potential issues in doing it that way? Seems like it could be a pretty straight forward solution to at least allow the user to write their own formatting method.

vertonghenb commented 4 years ago

It's possible to get the searchvalue each time the searchmethod is triggerd. CAUTION Some very RAW code examples here, but I think you'll get the point:

    private string searchTextCopy;
    private async Task<IEnumerable<Person>> GetPeopleLocal(string searchText)
    {
        searchTextCopy = searchText;
        return await Task.FromResult(People.Where(x => x.Firstname.ToLower().Contains(searchText.ToLower())).ToList());
    }

then you know the searchtext and you're able to use it in a renderfragment for your resulttemplate

<BlazoredTypeahead SearchMethod="GetPeopleLocal"
                   @bind-Value="SelectedPerson"
                   ShowDropDownOnFocus="true"
                   placeholder="Search by first name...">
    <SelectedTemplate Context="person">
        @person.Firstname
    </SelectedTemplate>
    <ResultTemplate Context="person">
        @((MarkupString)person.FullName?.HighlightKeyWords(searchText,"yellow",false))
    </ResultTemplate>
</BlazoredTypeahead>

Caution, I stole this code from somewhere, I didn't battle test it at all.

    public static class Extensions
    {
        /// <summary>
        /// Wraps matched strings in HTML span elements styled with a background-color
        /// </summary>
        /// <param name="text"></param>
        /// <param name="keywords">Comma-separated list of strings to be highlighted</param>
        /// <param name="cssClass">The Css color to apply</param>
        /// <param name="fullMatch">false for returning all matches, true for whole word matches only</param>
        /// <returns>string</returns>
        public static string HighlightKeyWords(this string text, string keywords, string cssClass, bool fullMatch)
        {
            if (text == String.Empty || keywords == String.Empty || cssClass == String.Empty || text is null)
                return text;

            if (keywords is null)
                return text;
            var words = keywords.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            if (!fullMatch)
                return words.Select(word => word.Trim()).Aggregate(text,
                             (current, pattern) =>
                             Regex.Replace(current,
                                             pattern,
                                               string.Format("<span style=\"background-color:{0}\">{1}</span>",
                                               cssClass,
                                               "$0"),
                                               RegexOptions.IgnoreCase));
            return words.Select(word => "\\b" + word.Trim() + "\\b")
                        .Aggregate(text, (current, pattern) =>
                                   Regex.Replace(current,
                                   pattern,
                                     string.Format("<span style=\"background-color:{0}\">{1}</span>",
                                     cssClass,
                                     "$0"),
                                     RegexOptions.IgnoreCase));

        }
    }

Which results in:

vertonghenb commented 4 years ago

@chucker, @erinnmclaughlin Any of you created a some production code for this? We might want to add it into the docs so we can close this issue.

chrissainty commented 4 years ago

Its great there is a manual way round this for people based on @vertonghenb comment. But it would be great to get this working automatically with the control, somehow.

vertonghenb commented 4 years ago

Its great there is a manual way round this for people based on @vertonghenb comment. But it would be great to get this working automatically with the control, somehow.

The issue is that we do not have control over the templates provided by the client. Unless we take control over the templates but that means clients will lose flexibility. The only thing we can do is :

chucker commented 4 years ago

Any of you created a some production code for this?

Nope. Haven't had a chance to look into this for now.

The issue is that we do not have control over the templates provided by the client.

One approach might be to add an out IRange[] highlightedCharacterRanges parameter to the expected SearchMethod.

I'm not sure this would work well for complex ResultTemplate scenarios, though.