Extend your Sitecore “Multilist Search Control” with a SPEAK treeview

advancedsearch2

In this post I would like to share with you guys how you can extend a control, in this case the Multilist with Search control, and add some very nice SPEAK functionality to it 🙂

Let say you want to add some search functionality to the “Multilist with Search” control. How about giving the editors the possibility to use a treeview for selecting one or many items. 🙂

We will add a third menu item(to the current two: Select all and Deselect all), called Advanced Search, which will open the treeview in a SPEAK dialog.
advanced4

We will divide the work into following:
1. Create a custom control(“Multilist with Search”) which will inherit BucketList and add some custom functionality to it.
2. Add the new control to the Sitecore Core database together with the third (new) menu item, called Advanced search.
3. Finally we will make a pop up(SPEAK dialog) which will present the tree view.

Create a custom control

Since the control “Multilist with Search” is of type Sitecore.Buckets.FieldTypes.BucketList, we will need the custom control to inherit the type. We want to open a SPEAK dialog window from the control, we will do this by adding a third menu item(next to Select all and Deselect all).

Let me give you guys a quick explanation how the custom control works. In the DoRender method we need to store some data in hidden fields, the current item and the start search item. The method HandleMessage will be called when you press a menu item(Select all, Deselect all and the new one Advanced search). If it finds the message “bucketsearchlist:openadvancedsearch” it will run the ClientPage.Start pipeline. The pipeline will call the OpenDialog method.

using System.Text.RegularExpressions;
using Sitecore.Buckets.FieldTypes;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace Sandbox.Website.Code.SitecoreCustom.Fields
{
	public class CustomMultilistSearch : BucketList
	{


		private const string PrefixCurrentItemId = "_currentItemId";
		private const string PrefixStartSearchLocation = "_startSearchLocation";

		public override void HandleMessage(Message message)
		{


			Assert.ArgumentNotNull((object)message, "message");
			base.HandleMessage(message);

			if (message["id"] != this.ID)
				return;

			if (message.Name != "bucketsearchlist:openadvancedsearch")
				return;

			Sitecore.Context.ClientPage.Start(this, "OpenDialog");

		}

		protected void OpenDialog(ClientPipelineArgs args)
		{
			if (args.IsPostBack)
			{
				Sitecore.Context.ClientPage.SendMessage(this, "item:refresh");
				return;
			}


			string url = GenerateSpeakUrl();

			SheerResponse.ShowModalDialog(new ModalDialogOptions(url)
			{
				Width = "600",
				Height = "600",
				Response = true,
				ForceDialogSize = true,
			});

			args.WaitForPostBack(true);
		}

		private string GenerateSpeakUrl()
		{
			string itemId = Sitecore.Context.ClientPage.ClientRequest.Form[$"{this.ID}{PrefixCurrentItemId}"];

			string loadPathId = Sitecore.Context.ClientPage.ClientRequest.Form[$"{this.ID}{PrefixStartSearchLocation}"];

			string rootItemInfo = GetRootItemInfo(loadPathId);

			string url =
				$"/sitecore/client/Sandbox/Applications/Dialogs/AdvancedGroupSearch?loadpathid={loadPathId}&rootiteminfo={rootItemInfo}&id={itemId}&database={Sitecore.Context.ContentDatabase.Name}&language={Sitecore.Context.Language.Name}&version=1";

			return url;
		}


		private string GetRootItemInfo(string loadPathtId)
		{
			Item item = Sitecore.Context.ContentDatabase.GetItem(new ID(loadPathtId));

			string imgSrc = Regex.Match(ThemeManager.GetIconImage(item, 16, 16, "", ""), "<img.+?src=[\"'](.+?)[\"'].*?>", RegexOptions.IgnoreCase).Groups[1].Value;

			return $"{item.DisplayName},{Sitecore.Context.ContentDatabase.Name},{loadPathtId},{imgSrc}";

		}

	
		private string GetStartSearchLocation()
		{
			NameValueCollection nameValues = StringUtil.GetNameValues(this.Source, '=', '&');

			foreach (string allKey in nameValues.AllKeys)
				nameValues[allKey] = HttpUtility.JavaScriptStringEncode(nameValues[allKey]);

			string startSearchLocation = nameValues["StartSearchLocation"];

			if (string.IsNullOrWhiteSpace(startSearchLocation))
				return ItemIDs.RootID.ToString();
			
			startSearchLocation = this.MakeFilterQueryable(startSearchLocation);

			return !Sitecore.Buckets.Extensions.StringExtensions.IsGuid(startSearchLocation) ? ItemIDs.RootID.ToString() : startSearchLocation;
		}

		protected override void DoRender(HtmlTextWriter output)
		{

			string startSearchLocation = GetStartSearchLocation();

			output.Write($"<input id=\"{this.ID}{PrefixCurrentItemId}\" type=\"hidden\" value=\"{this.ItemID}\" />");
			output.Write($"<input id=\"{this.ID}{PrefixStartSearchLocation}\" type=\"hidden\" value=\"{startSearchLocation}\" />");
			base.DoRender(output);

		}

	}
}

The method OpenDialog will open a SPEAK dialog by using the SheerResponse.ShowModalDialog method, we will add a proper url where the SPEAK dialog is located.

The SPEAK dialog will need the following parameters:
The actual item.
The start item to search from(which will be the root node for the treeview)

If you notice in the OpenDialog method we will also handle a postback. This means when the dialog is being closed it will call the command item:refresh to refresh the current page.

Add the new control to Sitecore Core

Next step will be to add the new control to Sitecore Core. We will put the control in /sitecore/system/Field types, an easy way is to just copy the item /sitecore/system/Field types/List Types/Multilist with Search.
custoncontrol

And here we add the third menu item with the message: bucketsearchlist:openadvancedsearch.
thirdmenuitem

Make the pop up(SPEAK dialog)

The SPEAK dialog is done in Sitecore 8.1, unfortunately it means that the itemTreeView is not in SPEAK 2.

Anyways lets fire up Sitecore Rocks and start creating the SPEAK dialog:
dialogspeak

If you notice I’ve put the treeview in a tabcontrol:
tabtreeviewspeak

What we have left is the Page Code file(js file).

define(["sitecore", "/-/speak/v1/experienceprofile/CintelUtl.js"], function (_sc, cintelUtil) {
	var dialog = _sc.Definitions.App.extend({

		initialized: function () {
			console.log("Advanced Search read");
			jQuery('body').addClass('sc-fullWidth');

			var self = this;
			self.OkButton.on("click", self.SaveSelection, this);
			self.TriggerButton.on("click", self.SetSelection, this);

			self.BindData();

			self.on("tree-isExpanded", self.TreeIsExpanded, self);
			
		},
		BindData: function () {
			var self = this;

			self.SetMessageToMessageBar("notification",
				"To set the selection from the multi search list control, please click on button - Set selection");
		
			self.GetGroupData(function (groupObject) {
				self.GroupData = groupObject;
				
				self.InitTreeView();

				var timer = setInterval(function () {
					if (!self.MyTreeView.get("isBusy")) {

						clearInterval(timer);
						
						self.ExpandTreeView();
					}

				}, 500);

			});


		},
		InitTreeView: function () {
			var self = this;

			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);

			treeView.attr("data-sc-rootitem", self.GroupData.rootItemInfo);
			treeView.attr("data-sc-loadpath", self.GroupData.loadPathId);

			self.MyTreeView.viewModel.getRoot().removeChildren();
			self.MyTreeView.viewModel.checkedItemIds([]);

			self.MyTreeView.viewModel.initialized();
		},
		TreeIsExpanded: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);

			treeView.dynatree("getRoot").visit(function (node) {
				node.expand(false);
			});

			self.ProgressIndicator.set("isVisible", false);

		},
		SetMessageToMessageBar: function (level, message, actions) {
			var self = this;
			self.TreeViewMessageBar.addMessage(level, {
				text: message,
				actions: [actions],
				closable: false
			});
		},
		SetSelection: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			$.each(self.GroupData.itemFieldMultiListSelections.split("|"), function (index, value) {
				treeView.dynatree("getTree").getNodeByKey(value).select();
			});
		},
		ExpandTreeView: function () {
			var self = this;

			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			
			var counter = 0;
			var lastCounted = 0;
			var rootNode = treeView.dynatree("getRoot");
			rootNode.select(false);

			
			treeView.dynatree("getRoot").visit(function (node) {
				node.expand();
			});


			$(".dynatree-container li ul").each(function (index, root) {
				checkEm(root);
			});

			var timer = setInterval(function () {
				if (lastCounted === counter) {
					clearInterval(timer);

					self.trigger('tree-isExpanded');


				}
				lastCounted = counter;

			}, 200);
			
			function checkEm(ulnode) {

				if (typeof (ulnode) == "undefined" || ulnode == null)
					return;

				$(ulnode).find('li').each(function (i, li) {

					//Only folders will we click on
					if ($(li).find("img")[0].src.indexOf("contract") === -1) {
						$(li).find("a").trigger("click");

						setTimeout(function () {
							counter += 1;

							var ulFound = $(li).find("ul");
							checkEm(ulFound);
					
							console.log("counter : " + counter);
						},
							100);
					}
				});
			}




		},
		SaveSelection: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			
			var selectedItems = $.grep(treeView.dynatree("getSelectedNodes"),
							function (node) { return !node.data.isFolder; })
						.map(function (node) { return node.data.key; });



			self.GroupData.itemFieldMultiListSelections = selectedItems.join("|");

			self.UpdateSitecoreData(function (readyState, status, responseText) {
				//Error
				if (readyState === 4 && (this.status === 404 || status === 500)) {
					self.SetMessageToMessageBar("error", responseText);
				}

				//Success
				if (readyState === 4 && this.status !== 404 && status !== 500) {
					window.top.dialogClose();
				}


			});

		},
		GetGroupData: function (callback) {

			var self = this;

			var groupObject = {};
			groupObject["id"] = cintelUtil.getQueryParam("id");
			groupObject["rootItemInfo"] = cintelUtil.getQueryParam("rootiteminfo");
			groupObject["loadPathId"] = cintelUtil.getQueryParam("loadpathid");
			groupObject["language"] = cintelUtil.getQueryParam("language");
			groupObject["version"] = cintelUtil.getQueryParam("version");
			groupObject["database"] = cintelUtil.getQueryParam("database");
			groupObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(groupObject["database"]);
			groupObject["itemUri"] = new _sc.Definitions.Data
				.ItemUri(groupObject["databaseUri"], groupObject["id"]);
			groupObject["itemVersionUri"] = new _sc.Definitions.Data
				.ItemVersionUri(groupObject["itemUri"],
					groupObject["language"],
					parseInt(groupObject["version"]));

			var db = new _sc.Definitions.Data.Database(groupObject["databaseUri"]);

			db.getItem(groupObject["itemVersionUri"],
				function (item) {

					groupObject["itemName"] = item.itemName;
					groupObject["itemDisplayname"] = item.$displayName;

					var itemFieldMultiListSelectionsField = item.getFieldById(self.Constants.CustomMultilistFieldId);
					groupObject["itemFieldMultiListSelections"] = itemFieldMultiListSelectionsField.value;


					callback(groupObject);

				});


		},
		GroupData: function () {
			var content;

			return content;
		},
		UpdateSitecoreData: function (callback) {
			var self = this;

			var request = new XMLHttpRequest();

			var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
				self.GroupData.id,
				self.GroupData.database,
				self.GroupData.language,
				self.GroupData.version);

			request.open("PATCH", url);

			request.setRequestHeader('Content-Type', 'application/json');

			var body = {
				'CustomMultilist': self.GroupData.itemFieldMultiListSelections,
			};

			request.send(JSON.stringify(body));

			request.onreadystatechange = function () {

				return callback(this.readyState, this.status, this.responseText);

			};
		},
		Constants: {
			"CustomMultilistFieldId": "{105BF825-145D-4278-8063-2A5E9472698E}"
		},
		StringFormat: function () {
			var s = arguments[0];
			for (var i = 0; i < arguments.length - 1; i++) {
				var reg = new RegExp("\\{" + i + "\\}", "gm");
				s = s.replace(reg, arguments[i + 1]);
			}
			return s;
		}


	});

	return dialog;
});

Let me give you a quick explanation on how it works.

In the BindData method we will get the item data by calling GetGroupData. Here we will get the selections from the Multilist Search control.

Next will be to reinitialize the treeview, yes you read it correct. Normally you set the treeview data in the properties from Sitecore Rocks but in this case we don’t know the root node(start node for the treeview) since we get it as a parameter. As you can see in method InitTreeView we need to do some tricks to make it work thanks to the Sitecore Community 🙂

We will then call the ExpandTreeView method, as you noticed I’ve put in an Interval function where we wait until the treeview is loaded.

In the ExpandTreeView method we need to expand all nodes in ALL levels, if we don’t do this we will NOT be able to set the the selections from the Multilist Search control.
When it’s done with the expanding we will close it nicely by calling/trigger method TreeIsExpanded.

To set the selections we do this in method SetSelection which is triggered from the button “Set selections”.
Finally when we hit the Save button we will call the SaveSelection method to gather the checked items from the treeview and then save them to Sitecore.

That’s it and keep doing some good SPEAK stuff out there.

That’s all for now folks 🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.