using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Xml.Linq; using ClientDependency.Core; using umbraco.cms.presentation.Trees; using umbraco.controls.Images; using umbraco.controls.Tree; using umbraco.IO; [assembly: WebResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerStyles.css", "text/css")] [assembly: WebResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerScripts.js", "application/x-javascript")] namespace umbraco.editorControls.MultiNodeTreePicker { /// /// The user interface to display to the content editor /// [ClientDependency(ClientDependencyType.Javascript, "ui/jqueryui.js", "UmbracoClient")] [ClientDependency(ClientDependencyType.Javascript, "ui/jquery.tooltip.min.js", "UmbracoClient")] [ClientDependency(ClientDependencyType.Javascript, "controls/Images/ImageViewer.js", "UmbracoRoot")] public class MNTP_DataEditor : Control, INamingContainer { #region Static Constructor /// /// This adds our filtered tree definition to the TreeDefinitionCollection at runtime /// instead of having to declare it in the database /// static MNTP_DataEditor() { if (TreeDefinitionCollection.Instance .Where(x => x.TreeType == typeof(FilteredContentTree)) .Count() == 0) { lock (m_Locker) { //double check lock.... if (TreeDefinitionCollection.Instance .Where(x => x.TreeType == typeof(FilteredContentTree)) .Count() == 0) { //need to add our tree definitions to the collection. //find the content tree to duplicate var contentTree = TreeDefinitionCollection.Instance.Where(x => x.Tree.Alias.ToUpper() == "CONTENT").Single(); var filteredContentTree = new TreeDefinition(typeof(FilteredContentTree), new umbraco.BusinessLogic.ApplicationTree(true, false, 0, contentTree.Tree.ApplicationAlias, "FilteredContentTree", contentTree.Tree.Title, contentTree.Tree.IconClosed, contentTree.Tree.IconOpened, "umbraco.editorControls", "MultiNodeTreePicker.FilteredContentTree", contentTree.Tree.Action), contentTree.App); //find the media tree to duplicate var mediaTree = TreeDefinitionCollection.Instance.Where(x => x.Tree.Alias.ToUpper() == "MEDIA").Single(); var filteredMediaTree = new TreeDefinition(typeof(FilteredMediaTree), new umbraco.BusinessLogic.ApplicationTree(true, false, 0, mediaTree.Tree.ApplicationAlias, "FilteredMediaTree", contentTree.Tree.Title, contentTree.Tree.IconClosed, contentTree.Tree.IconOpened, "umbraco.editorControls", "MultiNodeTreePicker.FilteredMediaTree", contentTree.Tree.Action), contentTree.App); //add it to the collection at runtime TreeDefinitionCollection.Instance.Add(filteredContentTree); TreeDefinitionCollection.Instance.Add(filteredMediaTree); } } } } #endregion /// /// Initializes a new instance of the class. /// public MNTP_DataEditor() { this.MediaTypesWithThumbnails = new string[] { "image" }; ShowThumbnailsForMedia = true; TreeToRender = "content"; MaxNodeCount = -1; MinNodeCount = 0; StartNodeId = uQuery.RootNodeId; ShowToolTips = true; ControlHeight = 200; } #region Static members /// /// Used for locking code blocks /// private static readonly object m_Locker = new object(); #endregion #region Protected members /// /// /// protected CustomValidator MinItemsValidator; /// /// /// protected CustomTreeControl TreePickerControl; /// /// /// protected Repeater SelectedValues; /// /// /// protected HiddenField PickedValue; /// /// /// protected HtmlGenericControl RightColumn; #endregion #region public Properties /// /// gets/sets the value based on an array of IDs selected /// public string[] SelectedIds { get { List val = new List(); var splitVals = PickedValue.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); //this will make sure only the node count specified is saved //into umbraco, even if people try to hack the front end for whatever reason. if (MaxNodeCount >= 0) { for (var i = 0; i < splitVals.Length; i++) { if (i < MaxNodeCount) { val.Add(splitVals[i]); } else break; } } else { val = splitVals.ToList(); } return val.ToArray(); } set { XmlValue = ConvertToXDocument(value); } } /// /// get/set the value for the selected nodes in xml format /// public XDocument XmlValue { get { return ConvertToXDocument(SelectedIds); } set { if (value == null) { SelectedValues.DataSource = null; PickedValue.Value = ""; } else { //set the data source for the repeater and hidden field var nodes = value.Descendants("nodeId"); SelectedValues.DataSource = nodes; PickedValue.Value = string.Join(",", nodes.Select(x => x.Value).ToArray()); } } } /// /// The property name being edited with the current data editor. This is used for the min items validation statement. /// public string PropertyName { get; set; } /// /// The tree type alias to render /// public string TreeToRender { get; set; } /// /// An xpath filter to match nodes that will be disabled from being clicked /// public string XPathFilter { get; set; } /// /// The minimum amount of nodes that can be selected /// public int MinNodeCount { get; set; } /// /// The maximum amount of nodes that can be selected /// public int MaxNodeCount { get; set; } /// /// The start node id /// public int StartNodeId { get; set; } /// /// The start node selection type /// public NodeSelectionType StartNodeSelectionType { get; set; } /// /// The xpath expression type to select the start node when the StartNodeSelectionType is XPath /// public XPathExpressionType StartNodeXPathExpressionType { get; set; } /// /// The XPath expression to use to determine the start node when the StartNodeSelectionType is XPath /// public string StartNodeXPathExpression { get; set; } /// /// Gets or sets a value indicating whether [show tool tips]. /// /// true if [show tool tips]; otherwise, false. /// Shows/Hides the tooltip info bubble. public bool ShowToolTips { get; set; } /// /// The XPathFilterType to match /// public XPathFilterType XPathFilterMatchType { get; set; } /// /// Gets or sets a value indicating whether [show thumbnails for media]. /// /// /// true if [show thumbnails for media]; otherwise, false. /// /// Whether or not to show thumbnails for media public bool ShowThumbnailsForMedia { get; set; } /// /// A list of media type names that can have thumbnails (i.e. 'image') /// public string[] MediaTypesWithThumbnails { get; set; } /// /// This is set by the data type and allows us to save a cookie value /// for persistence for the data type. /// public int DataTypeDefinitionId { get; set; } /// /// The height of the tree control box in pixels /// public int ControlHeight { get; set; } #endregion /// /// Initialize the control, make sure children are created /// /// An object that contains the event data. protected override void OnInit(EventArgs e) { base.OnInit(e); EnsureChildControls(); } /// /// Add the resources (sytles/scripts) /// /// protected override void OnLoad(EventArgs e) { base.OnLoad(e); //add the js/css required this.RegisterEmbeddedClientResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerStyles.css", umbraco.cms.businesslogic.datatype.ClientDependencyType.Css); this.RegisterEmbeddedClientResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerScripts.js", umbraco.cms.businesslogic.datatype.ClientDependencyType.Javascript); //update the tree type (we need to do this each time because i don't think view state works with these controls) switch (TreeToRender) { case "media": TreePickerControl.TreeType = "FilteredMediaTree"; TreePickerControl.App = "media"; break; case "content": default: TreePickerControl.TreeType = "FilteredContentTree"; TreePickerControl.App = "content"; break; } if (Page.IsPostBack) { //since it is a post back, bind the data source to the view state values XmlValue = ConvertToXDocument(SelectedIds); } //bind the repeater if theres a data source, or if there's no datasource but this is a postback (i.e. nodes deleted) if (SelectedValues.DataSource != null || Page.IsPostBack) { SelectedValues.DataBind(); } } /// /// Creates the child controls for this control /// protected override void CreateChildControls() { base.CreateChildControls(); EnsureChildControls(); //create the tree control TreePickerControl = new CustomTreeControl { ID = "TreePicker", IsDialog = true, ShowContextMenu = false, DialogMode = TreeDialogModes.id, Height = Unit.Pixel(ControlHeight), StartNodeID = StartNodeId }; //create the hidden field PickedValue = new HiddenField { ID = "PickedValue" }; //create the right column RightColumn = new HtmlGenericControl("div") { ID = "RightColumn" }; RightColumn.Attributes.Add("class", "right propertypane"); //create the repeater SelectedValues = new Repeater { //EnableViewState = false, ID = "SelectedValues", ItemTemplate = new SelectedItemsTemplate() }; SelectedValues.ItemDataBound += SelectedValues_ItemDataBound; //add the repeater to the right column RightColumn.Controls.Add(SelectedValues); MinItemsValidator = new CustomValidator() { ID = "MinItemsValidator", ErrorMessage = string.Format(MNTPResources.Val_MinItemsInvalid, MinNodeCount) }; MinItemsValidator.ServerValidate += new ServerValidateEventHandler(MinItemsValidator_ServerValidate); //add the controls this.Controls.Add(MinItemsValidator); this.Controls.Add(TreePickerControl); this.Controls.Add(PickedValue); this.Controls.Add(RightColumn); } /// /// Ensure the repeater is data bound /// public override void DataBind() { base.DataBind(); SelectedValues.DataBind(); } void MinItemsValidator_ServerValidate(object source, ServerValidateEventArgs args) { args.IsValid = true; if (MinNodeCount > 0 && SelectedIds.Length < MinNodeCount) { args.IsValid = false; } } /// /// Event handler for the selected node repeater. /// This will fill in all of the text values, icons, etc.. for nodes based on their ID. /// /// /// void SelectedValues_ItemDataBound(object sender, RepeaterItemEventArgs e) { var liSelectNode = (HtmlGenericControl)e.Item.FindControl("SelectedNodeListItem"); var lnkSelectNode = (HtmlAnchor)e.Item.FindControl("SelectedNodeLink"); var litSelectNodeName = (Literal)e.Item.FindControl("SelectedNodeText"); var infoButton = (HtmlAnchor)e.Item.FindControl("InfoButton"); //hide the info button if tooltips are hidden if (!ShowToolTips) { infoButton.Style.Add(HtmlTextWriterStyle.Display, "none"); } var thisNode = (XElement)e.Item.DataItem; int thisNodeId; if (int.TryParse(thisNode.Value, out thisNodeId)) { umbraco.cms.businesslogic.Content loadedNode; try { loadedNode = new umbraco.cms.businesslogic.Content(thisNodeId); //add the node id liSelectNode.Attributes["rel"] = thisNodeId.ToString(); //add the path to be referenced liSelectNode.Attributes["umb:nodedata"] = loadedNode.Path; lnkSelectNode.HRef = "javascript:void(0);"; litSelectNodeName.Text = loadedNode.Text; if (loadedNode.IsTrashed) { //need to flag this to be removed which will be done after all items are data bound liSelectNode.Attributes["rel"] = "trashed"; } else { //we need to set the icon if (loadedNode.ContentTypeIcon.StartsWith(".spr")) lnkSelectNode.Attributes["class"] += " " + loadedNode.ContentTypeIcon.TrimStart('.'); else { //it's a real icon, so make it a background image lnkSelectNode.Style.Add(HtmlTextWriterStyle.BackgroundImage, string.Format("url('{0}')", IconPath + loadedNode.ContentTypeIcon)); //set the nospr class since it's not a sprite lnkSelectNode.Attributes["class"] += " noSpr"; } //show the media preview if media and allowed if (TreeToRender == "media" && ShowThumbnailsForMedia) { var imgPreview = (ImageViewer)e.Item.FindControl("ImgPreview"); //show the thubmnail controls imgPreview.Visible = true; //add the item class var item = (HtmlGenericControl)e.Item.FindControl("Item"); item.Attributes["class"] += " thumb-item"; //item.Style.Add(HtmlTextWriterStyle.Height, "50px"); ////make the content sit beside the item //var inner = (HtmlGenericControl)e.Item.FindControl("InnerItem"); //inner.Style.Add(HtmlTextWriterStyle.Width, "224px"); //check if it's a thumbnail type element, we need to check both schemas if (MediaTypesWithThumbnails.Select(x => x.ToUpper()) .Contains(loadedNode.ContentType.Alias.ToUpper())) { imgPreview.MediaId = thisNodeId; imgPreview.DataBind(); } } } } catch (ArgumentException) { //the node no longer exists, so we display a msg litSelectNodeName.Text = "NODE NO LONGER EXISTS"; } } } /// /// set the nodekey to the id of this datatype /// /// /// this is how get the xpath out of the cookie to know how the tree knows how to filter things. /// generally the nodekey is used for a string id, but we'll use it for something different. /// /// protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); TreePickerControl.NodeKey = this.DataTypeDefinitionId.ToString(); SavePersistentValuesForTree(XPathFilter); } /// /// Override render to control the exact output of what is rendered this includes instantiating the jquery plugin /// /// The object that receives the server control content. /// /// Generally i don't like to do this but there's a few div's, etc... to render so this makes more sense. /// protected override void Render(HtmlTextWriter writer) { //
//
//
Select items
//
//
// //
//
//
//
RenderTooltip(writer); writer.AddAttribute("class", (!MinItemsValidator.IsValid ? "error " : "") + "multiNodePicker clearfix"); writer.AddAttribute("id", this.ClientID); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.AddAttribute("class", "header propertypane"); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.Write("Select Items"); writer.RenderEndTag(); writer.RenderEndTag(); writer.AddAttribute("class", "left propertypane"); writer.AddStyleAttribute(HtmlTextWriterStyle.Height, ((ControlHeight + 10).ToString() + "px")); writer.RenderBeginTag(HtmlTextWriterTag.Div); //add the tree control here TreePickerControl.RenderControl(writer); writer.RenderEndTag(); RightColumn.RenderControl(writer); //render the hidden field PickedValue.RenderControl(writer); writer.RenderEndTag(); //end multiNodePicker div var tooltipAjaxUrl = IOHelper.ResolveUrl(SystemDirectories.Umbraco) + @"/controls/Tree/CustomTreeService.asmx/GetNodeInfo"; //add jquery window load event to create the js tree picker var jsMethod = string.Format("jQuery('#{0}').MultiNodeTreePicker('{1}', {2}, '{3}', {4}, {5}, '{6}', '{7}');", TreePickerControl.ClientID, this.ClientID, MaxNodeCount, tooltipAjaxUrl, ShowToolTips.ToString().ToLower(), (TreeToRender == "media" && ShowThumbnailsForMedia).ToString().ToLower(), IOHelper.ResolveUrl(SystemDirectories.Umbraco), TreeToRender); var js = "jQuery(window).load(function() { " + jsMethod + " });"; writer.WriteLine(""); } /// /// converts a list of Ids to the XDocument structure /// /// The value. /// private XDocument ConvertToXDocument(IEnumerable val) { if (val.Count() > 0) { return new XDocument(new XElement("MultiNodePicker", new XAttribute("type", TreeToRender), val.Select(x => new XElement("nodeId", x.ToString())))); } else { //return null to support recursive values return null; //return an empty node set //return new XDocument(new XElement("MultiNodePicker")); } } /// /// this will render the tooltip object on the page so long as another /// one hasn't already been registered. There should only be one tooltip. /// private void RenderTooltip(HtmlTextWriter writer) { if (this.Page.Items.Contains("MNTPTooltip")) { return; } //render the tooltip holder //
//
//
//
//this.Page.Controls.AddAt(0, new LiteralControl("
")); writer.AddAttribute("id", "MNTPTooltip"); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.AddAttribute("class", "throbber"); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.RenderEndTag(); //end throbber writer.AddAttribute("class", "tooltipInfo"); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.RenderEndTag(); //end tooltipInfo writer.RenderEndTag(); //end tooltipo //ensure we add this to our page items so it's not duplicated this.Page.Items.Add("MNTPTooltip", true); } /// /// This will update the multi-node tree picker data which is used to store /// the xpath data and xpath match type for this control id. /// /// The xpath. /// /// This will save the data into a cookie and also into the request cookie. It must save /// it to both locations in case the request cookie has been changed and the request cookie /// is different than the response cookie. /// private void SavePersistentValuesForTree(string xpath) { //create the output cookie with all of the values of the request cookie var newCookie = HttpContext.Current.Response.Cookies[MNTP_DataType.PersistenceCookieName] ?? new HttpCookie(MNTP_DataType.PersistenceCookieName); //store the xpath for this data type definition newCookie.MntpAddXPathFilter(this.DataTypeDefinitionId, xpath); //store the match type newCookie.MntpAddXPathFilterType(this.DataTypeDefinitionId, XPathFilterMatchType); //store the start node id newCookie.MntpAddStartNodeId(this.DataTypeDefinitionId, StartNodeId); //store the start node selection type newCookie.MntpAddStartNodeSelectionType(this.DataTypeDefinitionId, StartNodeSelectionType); //store the start node xpath expression type newCookie.MntpAddStartNodeXPathExpressionType(this.DataTypeDefinitionId, StartNodeXPathExpressionType); //store the start node xpath expression newCookie.MntpAddStartNodeXPathExpression(this.DataTypeDefinitionId, StartNodeXPathExpression); //store the current editing node if found if (!string.IsNullOrEmpty(HttpContext.Current.Request["id"])) { var id = 0; if (int.TryParse(HttpContext.Current.Request["id"], out id)) { newCookie.MntpAddCurrentEditingNode(this.DataTypeDefinitionId, id); } } HttpContext.Current.Response.Cookies.Add(newCookie); //add it to the request cookies too, thus overriding any old data if (HttpContext.Current.Request.Cookies[MNTP_DataType.PersistenceCookieName] != null && HttpContext.Current.Request.Cookies[MNTP_DataType.PersistenceCookieName].Values.Count > 0) { //remove the incoming one and replace with new one HttpContext.Current.Request.Cookies.Remove(MNTP_DataType.PersistenceCookieName); } HttpContext.Current.Request.Cookies.Add(newCookie); } /// /// A reference path to where the icons are actually stored as compared to where the tree themes folder is /// private static readonly string IconPath = IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/images/umbraco/"; } }