Adding a telephone link option to the Episerver link editor

Extending the link editor in Episerver if not especially difficult, however, there are a few little snags you may run into that aren't altogether intuitive to solve. As such, I felt it warranted a detailed write-up where I'll try and call out the aforementioned inconveniences.

I think that adding a telephone number link is the perfect example, firstly, because it touches on everything you need to extend the link editor and, more importantly, because that's the scenario I was helping out with in the forum. Also, the only good example I could find was this blog post by Anders Hattestad which doesn't really get into the Dojo side of things (some would say wisely) and could be updated a little (it's from Episerver 7.5).

Here's what we'll end up with at the end of this blog post, the nice thing is that this approach will add it to URL, link list (LinkItemCollection) and rich-text properties:

The link editor in Episerver with a phone number option

There are only 2 real parts to this (maybe 2.5 if we include the module.config changes), so here we go:

1. The editor descriptor

First things first, we need to add our link option to the existing list. I can't take full credit for this code, it's an updated version of one shared by Marcelo on the forum. The use of the PlaceLast EditorDescriptorBehavior means that this is executed after the Episerver LinkEditorDescriptor and, as such, we can get the existing link options and add our custom one:

[EditorDescriptorRegistration(
    TargetType = typeof(string),
    UIHint = "HyperLink",
    EditorDescriptorBehavior = EditorDescriptorBehavior.PlaceLast)]
internal class CustomLinkEditorDescriptor : EditorDescriptor
{
    public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
    {
        base.ModifyMetadata(metadata, attributes);

        if (!metadata.EditorConfiguration.ContainsKey(("providers")))
        {
            return;
        }

        var providers = (metadata.EditorConfiguration["providers"] as IEnumerable<object>)?.ToList();

        if (providers == null)
        {
            return;
        }

        var phoneLink = new
        {
            Name = "PhoneNumber",
            Title = "Enter the phone number. The prefix tel will be added automatically.",
            DisplayName = "Phone Number",
            WidgetType = "scripts/PhoneValidationTextBox"
        };

        // This needs to go before the external link
        providers.Insert(GetIndex(metadata, "ExternalLink"), phoneLink);

        metadata.EditorConfiguration["providers"] = providers;
    }

    /// <summary>
    /// Get the index of an existing link.
    /// </summary>
    /// <param name="metadata">The metadata.</param>
    /// <param name="name">The name of the link.</param>
    /// <returns>The index.</returns>
    private static int GetIndex(ExtendedMetadata metadata, string name)
    {
        var jArray = JArray.FromObject(metadata.EditorConfiguration["providers"]);

        var item = jArray.FirstOrDefault(x => x.Value<string>("Name").Equals(name, StringComparison.Ordinal));

        return item == null ? jArray.Count : jArray.IndexOf(item);
    }
}

Annoyingly the HyperLinkModel is internal, which means that it's easiest to use an anonymous type when creating our phone link.

The other inconvenience here is that we need the phone link to go before the external link option, this is because otherwise the external link will keep getting populated with the entered telephone number when the editor goes to edit it (this would also be solvable by extending epi-cms/widget/HyperLinkSelector and overriding the _isValidEditorWidget function, but I want to introduce as few front-end changes as possible). As such, the only good reasonable way I could think of to get the index position was using a JArray.

2. The Dojo validation text box widget

Next up is the Dojo widget, this is based very closely on Episerver's epi-cms/form/EmailValidationTextBox:

define("scripts/PhoneValidationTextBox", [
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dijit/form/ValidationTextBox"
],
function (
    declare,
    lang,
    ValidationTextBox
) {

    var module = declare([ValidationTextBox], {
        // summary:
        //    Represents the phone number input textbox.
        // tags:
        //    internal

        invalidMessage: "Invalid phone number",

        // addTel: Boolean
        //      If true the value will always be prepended
        //      with the tel protocol
        addTel: true,

        validator: function (value, constraints) {
            // summary:
            //      If true the value will always be prepended
            //      Validate the text input with telephone number validation.
            // tags:
            //		override

            value = value || "";

            if (!this.required && this._isEmpty(value)) {
                return true;
            }

            // replace escaped sequences to enable/simplify regexp validation (\@ or "[email protected]"
            value = value.replace(/\\.{1}/g, "replaced").replace(/".*?"/g, "replaced");

            return module.validationRegex.test(value);
        },

        _getValueAttr: function () {

            var value = this.inherited(arguments);

            if (this.addTel) {
                // make sure the hyper link has tel: prefix
                value = value ? lang.trim(value) : "";
                if (value && value.indexOf("tel:") !== 0) {
                    value = "tel:" + value;
                }
            }

            return value;
        },

        _setValueAttr: function (value) {
            value = value ? value.replace("tel:", "") : "";

            this.inherited(arguments, [value]);
        }
    });

    // Simple and incomplete test for phone number likeness
    // only trying to stop the most common mistakes
    // This regex can be changed as required but needs to start
    // with an optional "tel:" (like so: (tel:)?)
    module.validationRegex = /^(tel:)?(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$/i;

    return module;

});

The validation regex here is very generous and accepts a lot of valid and invalid phone number combinations (I didn't write this monster, just found it on Stack Overflow somewhere!). It's really easy to replace this, however, the important part is that you keep the optional “tel:” token at the beginning (as per the comment). This is because the validator function is also used when deciding which link option to populate when the editor opens the link modal, as it'll have “tel:” prepended in this scenario we need to make sure it still valdiates.

2.5. Updating the module.config

Last part is the easiest, you'll need to add the following line to your module.config (or create it if it doesn't exist):

<?xml version="1.0" encoding="utf-8"?>
<module>
  <dojo>
    <paths>
      <add name="scripts" path="Scripts" />
    </paths>
  </dojo>
</module>

The path specified above is where you should add the JavaScript to your solution (by default Episerver is expecting it in a ClientResources folder, so the above is actually ClientResources/Scripts). If you decide to change the path name just make sure you align it across the editor descriptor, Dojo widget and this configuration file.

Conclusion

At this point it should be working and hopefully editors can now enter phone number to their hearts’ content.

As per usual, let me know if you have any comments or improvements.

Comments

Hello, sorry if this is a silly question, but concerning point 1 about the EditorDescriptorRegistration. Do I create a new .cs file at Business/EditorDescriptors/ containing my new custom EditorDescriptorRegistration ?

PartisanEntity

It depends on your solution where you'd want to create it. But yeah, you can pretty much copy and paste the example above to that location.

Jake

Our editors have asked for the ability to select a "button type" when they create a link. The button type then defines which css is inserted into the html. Your blog post was suggested to me on the forum here (https://world.episerver.com/forum/developer-forum/Developer-to-developer/Thread-Container/2020/6/add-field-to-linkitemcollection-/) as a good starting point.

PartisanEntity

Hi Jake, thanks for your great article. I am trying to extend the EPiServer link editor myself. Your article helped me a lot. One minor remark: a comment in the Javascript says "...has mailto: prefix", which should be "...has tel: prefix".

Sander

Thanks. I've fixed the offending comment!

Jake