xamluwptextblockrichtextblock

Auto detect URL, phone number, email in TextBlock


I would like to automatically highlight URL, Email and phone number in UWP. It is possible in Android but it seems this features has been forgotten by Microsoft. In my use case, I get the text from a web service, so I don't know the text format which is a user text input on the web platform.


Solution

  • 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:

    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);
        }