Auto detect URL, phone number, email in TextBlock

北城余情 提交于 2019-12-03 22:19:54
Vincent

The platform is not supporting this feature (yet). When I've to do the same thing, I've ended with my own solution which is to:

  • create an attached property receiving the text to format
  • use some regex to extract the URL, phone numbers, email addresses from it
  • generate an Inlines collection which I'm injecting to the attached TextBlock control

The regex used are covering a lot of cases but some edge cases can be still missing.

It is used this way:

<TextBlock  uwpext:TextBlock.InteractiveText="Here is a link www.bing.com to send to a@a.com or 0000000000" />

The attached property code:

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The regex to detect the URL from the text content
    /// It comes from https://gist.github.com/gruber/249502 (http://daringfireball.net/2010/07/improved_regex_for_matching_urls)
    /// </summary>
    private static readonly Regex UrlRegex = new Regex(@"(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'"".,<>?«»“”‘’]))", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(500));

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The regex to detect the email addresses
    /// It comes from https://msdn.microsoft.com/en-us/library/01escwtf.aspx
    /// </summary>
    private static readonly Regex EmailRegex    = new Regex(@"(?("")("".+?(?<!\\)""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(500));

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The regex to detect the phone numbers from the raw message
    /// </summary>
    private static readonly Regex PhoneRegex    = new Regex(@"\+?[\d\-\(\)\. ]{5,}", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The default prefix to use to convert a relative URI to an absolute URI
    /// The Windows RunTime is only working with absolute URI
    /// </summary>
    private const string    RelativeUriDefaultPrefix    = "http://";


    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The dependency property to generate an interactive text in a text block.
    /// When setting this property, we will parse the value and transform the hyperlink or the email address to interactive fields that the user can interact width.
    /// The raw text will be parsed and convert to a collection of inlines.
    /// </summary>
    public static readonly DependencyProperty InteractiveTextProperty = DependencyProperty.RegisterAttached("InteractiveText", typeof(string), typeof(TextBlock), new PropertyMetadata(null, OnInteractiveTextChanged));

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// The event callback for the interactive text changed event
    /// We will parse the raw text and generate the inlines that will wrap the interactive items (URL...)
    /// </summary>
    /// <param name="d">the object which has raised the event</param>
    /// <param name="e">the change information</param>
    private static void OnInteractiveTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBlock   = d as Windows.UI.Xaml.Controls.TextBlock;
        if(textBlock == null) return;

        // we remove all the inlines
        textBlock.Inlines.Clear();

        // if we have no data, we do not need to go further
        var rawText = e.NewValue as string;
        if(string.IsNullOrEmpty(rawText)) return;


        var lastPosition    = 0;
        var matches         = new Match[3];
        do
        {
            matches[0]  = UrlRegex.Match(rawText, lastPosition);
            matches[1]  = EmailRegex.Match(rawText, lastPosition);
            matches[2]  = PhoneRegex.Match(rawText, lastPosition);

            var firstMatch  = matches.Where(x => x.Success).OrderBy(x => x.Index).FirstOrDefault();
            if(firstMatch == matches[0])
            {
                // the first match is an URL
                CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
                lastPosition    = CreateUrlElement(textBlock, firstMatch);
            }
            else if(firstMatch == matches[1])
            {
                // the first match is an email
                CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
                lastPosition    = CreateContactElement(textBlock, firstMatch, null);
            }
            else if(firstMatch == matches[2])
            {
                // the first match is a phonenumber
                CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
                lastPosition    = CreateContactElement(textBlock, null, firstMatch);
            }
            else
            {
                // no match, we add the whole text
                textBlock.Inlines.Add(new Run { Text = rawText.Substring(lastPosition) });
                lastPosition    = rawText.Length;
            }  
        }
        while(lastPosition < rawText.Length);
    }

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// This method will extract a fragment of the raw text string, create a Run element with the fragment and
    /// add it to the textblock inlines collection
    /// </summary>
    /// <param name="textBlock">the textblock where to add the run element</param>
    /// <param name="rawText">the raw text where the fragment will be extracted</param>
    /// <param name="startPosition">the start position to extract the fragment</param>
    /// <param name="endPosition">the end position to extract the fragment</param>
    private static void CreateRunElement(Windows.UI.Xaml.Controls.TextBlock textBlock, string rawText, int startPosition, int endPosition)
    {
        var fragment    = rawText.Substring(startPosition, endPosition - startPosition);
        textBlock.Inlines.Add(new Run { Text = fragment });
    }

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// Create an URL element with the provided match result from the URL regex
    /// It will create the Hyperlink element that will contain the URL and add it to the provided textblock
    /// </summary>
    /// <param name="textBlock">the textblock where to add the hyperlink</param>
    /// <param name="urlMatch">the match for the URL to use to create the hyperlink element</param>
    /// <returns>the newest position on the source string for the parsing</returns>
    private static int CreateUrlElement(Windows.UI.Xaml.Controls.TextBlock textBlock, Match urlMatch)
    {
        Uri targetUri;
        if(Uri.TryCreate(urlMatch.Value, UriKind.RelativeOrAbsolute, out targetUri))
        {
            var link            = new Hyperlink();
            link.Inlines.Add(new Run { Text= urlMatch.Value });

            if(targetUri.IsAbsoluteUri)
                link.NavigateUri    = targetUri;
            else
                link.NavigateUri    = new Uri(RelativeUriDefaultPrefix + targetUri.OriginalString);


            textBlock.Inlines.Add(link);
        }
        else
        {
            textBlock.Inlines.Add(new Run { Text= urlMatch.Value });
        }

        return urlMatch.Index + urlMatch.Length;
    }

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// Create a hyperlink element with the provided match result from the regex that will open the contact application
    /// with the provided contact information (it should be a phone number or an email address
    /// This is used only if the email address / phone number is not prefixed with the mailto: / tel: scheme
    /// It will create the Hyperlink element that will contain the email/phone number hyperlink and add it to the provided textblock.
    /// Clicking on the link will open the contact application
    /// </summary>
    /// <param name="textBlock">the textblock where to add the hyperlink</param>
    /// <param name="emailMatch">the match for the email to use to create the hyperlink element. Set to null if not available but at least one of emailMatch and phoneMatch must be not null.</param>
    /// <param name="phoneMatch">the match for the phone number to create the hyperlink element. Set to null if not available but at least one of emailMatch and phoneMatch must be not null.</param>
    /// <returns>the newest position on the source string for the parsing</returns>
    private static int CreateContactElement(Windows.UI.Xaml.Controls.TextBlock textBlock, Match emailMatch, Match phoneMatch)
    {
        var currentMatch    = emailMatch ?? phoneMatch;

        var link            = new Hyperlink();
        link.Inlines.Add(new Run { Text= currentMatch.Value });
        link.Click          += (s, a) =>
        {
            var contact     = new Contact();
            if(emailMatch != null)  contact.Emails.Add(new ContactEmail { Address   = emailMatch.Value  });
            if(phoneMatch != null)  contact.Phones.Add(new ContactPhone { Number    = phoneMatch.Value.StripNonDigitsCharacters() });

            ContactManager.ShowFullContactCard(contact, new FullContactCardOptions());
        };

        textBlock.Inlines.Add(link);
        return currentMatch.Index + currentMatch.Length;
    }

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// Return the InteractiveText value on the provided object
    /// </summary>
    /// <param name="obj">the object to query</param>
    /// <returns>the InteractiveText value</returns>
    public static string GetInteractiveText(DependencyObject obj)
    {
        return (string) obj.GetValue(InteractiveTextProperty);
    }

    // -------------------------------------------------------------------------------------------
    /// <summary>
    /// SEt the InteractiveText value on the provided object
    /// </summary>
    /// <param name="obj">the object to query</param>
    /// <param name="value">the value to set</param>
    public static void SetInteractiveText(DependencyObject obj, string value)
    {
        obj.SetValue(InteractiveTextProperty, value);
    }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!