Posted in: Comments

First of all, big thanks to Anders Hattestad and all his great contributions to the community! Some months ago he posted a great post on how to use categories in a better way. I´ve taken his code and refined it a bit.

 I started with the gui first. The dropdown and the inputs needed some more styling to match EPiServer’s gui.

Then I realized that all page categories were saved to the property, so if you where retrieving the property by

CurrentPage["BlogPostTags"]

you would get all categories returned, not the ones just specified in the property. And you could not deselect any category on the page, just add new ones.

By default the property doesn´t hide the categories on the “Categories” tab. If you want to hide all categories just go to admin mode and change the visibility to false on the root category for the property.

namespace DV.Properties
{
    using System;
    using System.Collections.Generic;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using EPiServer;
    using EPiServer.Core;
    using EPiServer.DataAbstraction;
    using EPiServer.PlugIn;
    using EPiServer.Web.PropertyControls;

    [Serializable]
    [PageDefinitionTypePlugIn(
        DisplayName = "Category selection - select or add",
        Description = "Category selection - select or add")]
    public class PropertySelectOrAddCategory : PropertyCategory
    {
        public override EPiServer.Core.IPropertyControl CreatePropertyControl()
        {
            return new PropertySelectOrAddCategoryControl();
        }
    }

    public class AttachEvents : PlugInAttribute
    {
        public static void Start()
        {
            DataFactory.Instance.CreatingPage += new PageEventHandler(Instance_SavingPage);
            DataFactory.Instance.SavingPage += new PageEventHandler(Instance_SavingPage);
        }

        static void Instance_SavingPage(object sender, EPiServer.PageEventArgs e)
        {
            CategoryList oldValue = e.Page.Category;
            CategoryList newValue = new CategoryList();
            bool customPropertyExists = false;

            foreach (PropertyData prop in e.Page.Property)
            {
                if (prop.GetType() == typeof(PropertySelectOrAddCategory)
                {
                    customPropertyExists = true;

                    foreach (int catId in oldValue)
                    {
                        Category thisCat = Category.Find(catId);

                        if (thisCat.Parent == null || thisCat.Parent.Name != prop.Name)
                        {
                            newValue.Add(catId);
                        }
                    }

                    foreach (int catId in (prop as PropertySelectOrAddCategory).Category)
                    {
                        newValue.Add(catId);
                    }
                }
            }

            if (customPropertyExists)
            {
                e.Page.Property["PageCategory"].Value = newValue;
            }
        }
    }

    public class PropertySelectOrAddCategoryControl : PropertyStringControl
    {
        List<CheckBox> CheckBoxList = new List<CheckBox>();
        TextBox oldItemsBox;
        TextBox addNewBox;

        public override void CreateEditControls()
        {
            #region Display controls

            Panel outerDiv = new Panel();
            outerDiv.Style.Add("position", "relative");
            outerDiv.Style.Add("overflow", "visible");
            this.Controls.Add(outerDiv);

            addNewBox = new TextBox();
            addNewBox.ID = this.Name + "NewText";
            addNewBox.CssClass = "episize240";
            addNewBox.Attributes.Add("autocomplete", "off");
            outerDiv.Controls.Add(addNewBox);

            Button addBtn = new Button();
            addBtn.Text = "+";
            addBtn.CssClass = "epismallbutton";
            outerDiv.Controls.Add(addBtn);

            Panel theDiv = new Panel();
            theDiv.ID = this.Name + "Options";
            theDiv.CssClass = "AutoCompleteWrapper";
            outerDiv.Controls.Add(theDiv);

            oldItemsBox = new TextBox();
            oldItemsBox.Style.Add("display", "none");
            oldItemsBox.ID = this.Name + "Old";
            this.Controls.Add(oldItemsBox);

            EPiServer.ClientScript.ScriptManager current = EPiServer.ClientScript.ScriptManager.Current;

            if (current != null)
            {
                current.AddEventListener(addBtn, new EPiServer.ClientScript.Events.DisablePageLeaveEvent(EPiServer.ClientScript.EventType.Click));
            }

            #endregion

            Category root = GetRoot();
            Dictionary<string, CategoryItem> existing = new Dictionary<string, CategoryItem>();
            Dictionary<string, CategoryItem> shown = new Dictionary<string, CategoryItem>();
            List<Category> all = new List<Category>();

            #region Find what to show

            //First time
            CategoryItem item = null;
            foreach (EPiServer.DataAbstraction.Category cat in root.Categories)
            {
                item = new CategoryItem(cat);
                if (!existing.ContainsKey(item.Key))
                {
                    existing.Add(item.Key, item);
                    if (CurrentPage.Category.MemberOf(cat.ID))
                    {
                        shown.Add(item.Key, item);
                    }
                }
            }

            //second time
            if (this.Page.Request[oldItemsBox.UniqueID] != null)
            {
                string[] split = this.Page.Request[oldItemsBox.UniqueID].Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

                foreach (string s in split)
                {
                    item = new CategoryItem(s);

                    if (!existing.ContainsKey(item.Key))
                    {
                        existing.Add(item.Key, item);
                        shown.Add(item.Key, item);
                    }
                    else if (!shown.ContainsKey(item.Key))
                    {
                        shown.Add(item.Key, existing[item.Key]);
                    }
                }

                if (!string.IsNullOrEmpty(this.Page.Request[addNewBox.UniqueID]))
                {
                    item = new CategoryItem(this.Page.Request[addNewBox.UniqueID]);

                    if (!existing.ContainsKey(item.Key))
                    {
                        existing.Add(item.Key, item);
                        shown.Add(item.Key, item);
                    }
                    else if (!shown.ContainsKey(item.Key))
                    {
                        shown.Add(item.Key, existing[item.Key]);
                    }
                }
            }

            #endregion

            foreach (CategoryItem s in shown.Values)
            {
                this.Controls.Add(new LiteralControl("<div>"));
                CheckBox check = new CheckBox();
                check.ID = s.Key;
                check.Text = s.Name;
                check.Checked = true;
                this.Controls.Add(check);
                this.Controls.Add(new LiteralControl("</div>"));

                this.CheckBoxList.Add(check);
            }

            #region AutoComplete

            ScriptManager.RegisterClientScriptBlock(this, typeof(PropertySelectOrAddCategory), "Startup" + this.Name, jsScript, false);
            string dataArray = "";
            string scriptString = "<script language=\"JavaScript\">";

            foreach (CategoryItem sub in existing.Values)
            {
                if (dataArray != "")
                    dataArray += ",";
                dataArray += "\"" + sub.Name + "\"";
            }
            scriptString += "\n new AutoComplete(" + System.Environment.NewLine +
                " [" + dataArray + "]," + System.Environment.NewLine +
                " document.getElementById('" + addNewBox.ClientID + "')," + System.Environment.NewLine +
                " document.getElementById('" + theDiv.ClientID + "')," + System.Environment.NewLine +
                " 25)";
            scriptString += "</script>";
            ScriptManager.RegisterStartupScript(this, typeof(PropertySelectOrAddCategory), "Startup" + this.Name, scriptString, false);

            #endregion
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            if (oldItemsBox != null)
            {
                string saveVal = "";

                foreach (CheckBox check in CheckBoxList)
                {
                    if (saveVal != "")
                    {
                        saveVal += ";";
                    }

                    saveVal += check.Text;
                }

                oldItemsBox.Text = saveVal;
                addNewBox.Text = "";
            }
        }

        public override void ApplyEditChanges()
        {
            CategoryList result = new CategoryList();

            Category root = GetRoot();

            foreach (CheckBox box in CheckBoxList)
            {
                if (this.Page.Request[box.UniqueID] != null)
                {
                    Category thisCat = root.FindChild(box.Text);

                    if (thisCat == null)
                    {
                        thisCat = EnsureCategoryExists(root, box.Text, box.Text, true);
                    }

                    result.Add(thisCat.ID);
                }
            }

            SetValue(result);
        }

        public Category GetRoot()
        {
            Category root = Category.GetRoot();

            return EnsureCategoryExists(root, this.Name, this.Name, false);
        }

        public Category EnsureCategoryExists(Category parent, string name, string description, bool selectable)
        {
            Category result = parent.FindChild(name);

            if (result == null)
            {
                result = new Category(name, description);
                result.Parent = parent;
                result.Selectable = selectable;
                result.Available = true;
                result.Save();
            }

            return result;
        }

        public PropertySelectOrAddCategory PropertySelectOrAddCategory
        {
            get
            {
                return PropertyData as PropertySelectOrAddCategory;
            }
        }

        string jsScript = @"
        <style>
            .AutoCompleteWrapper {
                position:absolute;
                visibility:hidden;
                left:20px;
                top:20px;
                z-index:100;
                border:1px solid #000;
                background:#fff;
            }
            .AutoCompleteWrapper div {
                padding:5px 10px;
                cursor:hand;
                cursor:pointer;
            }
            .AutoCompleteBackground {
                background-color:#fff;
            }
            .AutoCompleteHighlight {
                background-color:#ccc;
            }
        </style>


        <script language=""javascript"">

            function AutoCompleteDB() {
                // set the initial values.
                this.bEnd = false;
                this.nCount = 0;
                this.aStr = new Object;
            }

            AutoCompleteDB.prototype.add = function(str) {
                // increment the count value.
                this.nCount++;

                // if at the end of the string, flag this node as an end point.
                if (str == """")
                    this.bEnd = true;
                else {
                    // otherwise, pull the first letter off the string
                    var letter = str.substring(0, 1);
                    var rest = str.substring(1, str.length);

                    // and either create a child node for it or reuse an old one.
                    if (!this.aStr[letter]) this.aStr[letter] = new AutoCompleteDB();
                    this.aStr[letter].add(rest);
                }
            }

            AutoCompleteDB.prototype.getCount = function(str, bExact) {
                // if end of search string, return number
                if (str == """")
                    if (this.bEnd && bExact && (this.nCount == 1)) return 0;
                else return this.nCount;

                // otherwise, pull the first letter off the string
                var letter = str.substring(0, 1);
                var rest = str.substring(1, str.length);

                // and look for case-insensitive matches
                var nCount = 0;
                var lLetter = letter.toLowerCase();
                if (this.aStr[lLetter])
                    nCount += this.aStr[lLetter].getCount(rest, bExact && (letter == lLetter));

                var uLetter = letter.toUpperCase();
                if (this.aStr[uLetter])
                    nCount += this.aStr[uLetter].getCount(rest, bExact && (letter == uLetter));

                return nCount;
            }

            AutoCompleteDB.prototype.getStrings = function(str1, str2, outStr) {
                if (str1 == """") {
                    // add matching strings to the array
                    if (this.bEnd)
                        outStr.push(str2);

                    // get strings for each child node
                    for (var i in this.aStr)
                        this.aStr[i].getStrings(str1, str2 + i, outStr);
                }
                else {
                    // pull the first letter off the string
                    var letter = str1.substring(0, 1);
                    var rest = str1.substring(1, str1.length);

                    // and get the case-insensitive matches.
                    var lLetter = letter.toLowerCase();
                    if (this.aStr[lLetter])
                        this.aStr[lLetter].getStrings(rest, str2 + lLetter, outStr);

                    var uLetter = letter.toUpperCase();
                    if (this.aStr[uLetter])
                        this.aStr[uLetter].getStrings(rest, str2 + uLetter, outStr);
                }
            }


            function AutoComplete(aStr, oText, oDiv, nMaxSize) {
                // initialize member variables
                this.oText = oText;
                this.oDiv = oDiv;
                this.nMaxSize = nMaxSize;
                if (!oText)
                    return;
                    
                // preprocess the texts for fast access
                this.db = new AutoCompleteDB();
                var i, n = aStr.length;
                for (i = 0; i < n; i++) {
                    this.db.add(aStr[i]);
                }

                // attach handlers to the text-box
                oText.AutoComplete = this;
                oText.onkeyup = AutoComplete.prototype.onTextChange;
                oText.onblur = AutoComplete.prototype.onTextBlur;
            }

            AutoComplete.prototype.onTextBlur = function() {
                this.AutoComplete.onblur();
            }

            AutoComplete.prototype.onblur = function() {
                this.oDiv.style.visibility = ""hidden"";
            }

            AutoComplete.prototype.onTextChange = function() {
                this.AutoComplete.onchange();
            }

            AutoComplete.prototype.onDivMouseDown = function() {
                this.AutoComplete.oText.value = this.innerHTML;
            }

            AutoComplete.prototype.onDivMouseOver = function() {
                this.className = ""AutoCompleteHighlight"";
            }

            AutoComplete.prototype.onDivMouseOut = function() {
                this.className = ""AutoCompleteBackground"";
            }

            AutoComplete.prototype.onchange = function() {
                var txt = this.oText.value;

                // count the number of strings that match the text-box value
                var nCount = this.db.getCount(txt, true);

                // if a suitable number then show the popup-div
                if ((this.nMaxSize == -1) || ((nCount < this.nMaxSize) && (nCount > 0))) {
                    // clear the popup-div.
                    while (this.oDiv.hasChildNodes())
                        this.oDiv.removeChild(this.oDiv.firstChild);

                    // get all the matching strings from the AutoCompleteDB
                    var aStr = new Array();
                    this.db.getStrings(txt, """", aStr);

                    // add each string to the popup-div
                    var i, n = aStr.length;
                    for (i = 0; i < n; i++) {
                        var oDiv = document.createElement('div');
                        this.oDiv.appendChild(oDiv);
                        oDiv.innerHTML = aStr[i];
                        oDiv.onmousedown = AutoComplete.prototype.onDivMouseDown;
                        oDiv.onmouseover = AutoComplete.prototype.onDivMouseOver;
                        oDiv.onmouseout = AutoComplete.prototype.onDivMouseOut;
                        oDiv.AutoComplete = this;
                    }
                    this.oDiv.style.visibility = ""visible"";
                }
                else // hide the popup-div
                {
                    this.oDiv.innerHTML = """";
                    this.oDiv.style.visibility = ""hidden"";
                }
            }
        </script>";

        public class CategoryItem
        {
            public CategoryItem(string name)
            {
                Name = name;
            }

            public CategoryItem(EPiServer.DataAbstraction.Category cat)
            {
                Cat = cat;
                Name = cat.Name;
            }

            public EPiServer.DataAbstraction.Category Cat;
            public string Name;
            string key;

            public string Key
            {
                get
                {
                    if (key == null)
                    {
                        key = Name.ToLower().Trim().Replace(" ", "").Replace(".", "");
                    }

                    return key;
                }
            }
        }
    }
}