diff --git a/components/editorControls/MultiNodeTreePicker/BaseTreeExtensions.cs b/components/editorControls/MultiNodeTreePicker/BaseTreeExtensions.cs new file mode 100644 index 0000000000..e13bac456d --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/BaseTreeExtensions.cs @@ -0,0 +1,194 @@ +using System; +using System.Linq; +using System.Web; +using umbraco.cms.businesslogic; +using umbraco.cms.presentation.Trees; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// BaseTree extensions for MultiNodeTreePicker. + /// + public static class BaseTreeExtensions + { + + internal const int NoAccessId = -123456789; + internal const int NoChildNodesId = -897987654; + + /// + /// Determines if it needs to render a null tree based on the start node id and returns true if it is the case. + /// + /// + /// + /// + /// + /// + internal static bool SetNullTreeRootNode(this BaseTree tree, int startNodeId, ref XmlTreeNode rootNode, string app) + { + if (startNodeId == NoAccessId) + { + rootNode = new NullTree(app).RootNode; + rootNode.Text = "You do not have permission to view this tree"; + rootNode.HasChildren = false; + rootNode.Source = string.Empty; + return true; + } + + if (startNodeId == NoChildNodesId) + { + rootNode = new NullTree(app).RootNode; + rootNode.Text = "[No start node found]"; + rootNode.HasChildren = false; + rootNode.Source = string.Empty; + return true; + } + + return false; + } + + /// + /// Used to determine the start node id while taking into account a user's security + /// + /// + /// + /// + /// + internal static int GetStartNodeId(this BaseTree tree, Content definedStartNode, Content userStartNode) + { + if (userStartNode == null) + { + throw new ArgumentNullException("userStartNode"); + } + + //the output start node id + var determinedStartNodeId = uQuery.RootNodeId; + + if (definedStartNode == null) + { + //if the defined (desired) start node is null (could not be found), return NoChildNodesId + determinedStartNodeId = NoChildNodesId; + } + else if (definedStartNode.Id == uQuery.RootNodeId) + { + //if the id is -1, then the start node is the user's start node + determinedStartNodeId = userStartNode.Id; + } + else if (definedStartNode.Path.Split(',').Contains(userStartNode.Id.ToString())) + { + //If User's start node id is found in the defined path, then the start node id + //can (allowed) be the defined path. + //This should always work for administrator (-1) + + determinedStartNodeId = definedStartNode.Id; + } + else if (userStartNode.Path.Split(',').Contains(definedStartNode.Id.ToString())) + { + //if the defined start node id is found in the user's path, then the start node id + //can only be the user's, not the actual start + determinedStartNodeId = userStartNode.Id; + } + else if (!definedStartNode.Path.Split(',').Contains(userStartNode.Id.ToString())) + { + //they should not have any access! + determinedStartNodeId = NoAccessId; + } + + return determinedStartNodeId; + } + + /// + /// Returns the data type id for the current base tree + /// + /// + /// The data type definition id is persisted between request as a query string. + /// This is used to retreive values from the cookie which are easier persisted values + /// than trying to append everything to custom query strings. + /// + /// + /// + internal static int GetDataTypeId(this BaseTree tree) + { + var id = -1; + int.TryParse(tree.NodeKey, out id); + return id; + } + + /// + /// return the xpath statement stored in the cookie for this control id + /// + /// + /// + /// + internal static string GetXPathFromCookie(this BaseTree tree, int dataTypeDefId) + { + //try to read an existing cookie + var cookie = HttpContext.Current.Request.Cookies["MultiNodeTreePicker"]; + if (cookie != null && cookie.Values.Count > 0) + { + return cookie.MntpGetXPathFilter(dataTypeDefId); + } + return ""; + } + + + /// + /// Returns the xpath filter from the cookie for the current data type + /// + /// + /// + /// + internal static XPathFilterType GetXPathFilterTypeFromCookie(this BaseTree tree, int dataTypeDefId) + { + //try to read an existing cookie + var cookie = HttpContext.Current.Request.Cookies["MultiNodeTreePicker"]; + if (cookie != null && cookie.Values.Count > 0) + { + return cookie.MntpGetXPathFilterType(dataTypeDefId); + } + return XPathFilterType.Disable; + } + + /// + /// Helper method to return the persisted cookied value for the tree + /// + /// + /// + /// + /// + /// + internal static T GetPersistedCookieValue(this BaseTree tree, Func output, T defaultVal) + { + var cookie = HttpContext.Current.Request.Cookies["MultiNodeTreePicker"]; + if (cookie != null && cookie.Values.Count > 0) + { + return output(cookie); + } + return defaultVal; + } + + /// + /// This will return the normal service url based on id but will also ensure that the data type definition id is passed through as the nodeKey param + /// + /// The tree. + /// The id. + /// The data type def id. + /// + /// + /// We only need to set the custom source to pass in our extra NodeKey data. + /// By default the system will use one or the other: Id or NodeKey, in this case + /// we are sort of 'tricking' the system and we require both. + /// Umbraco allows you to theoretically pass in any source as long as it meets the standard + /// which means you can pass around any arbitrary data to your trees in the form of a query string, + /// though it's just a bit convoluted to do so. + /// + internal static string GetTreeServiceUrlWithParams(this BaseTree tree, int id, int dataTypeDefId) + { + var url = tree.GetTreeServiceUrl(id); + //append the node key + return url + "&nodeKey=" + dataTypeDefId; + } + + + + } +} diff --git a/components/editorControls/MultiNodeTreePicker/FilteredContentTree.cs b/components/editorControls/MultiNodeTreePicker/FilteredContentTree.cs new file mode 100644 index 0000000000..fc92e8582f --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/FilteredContentTree.cs @@ -0,0 +1,221 @@ +using System; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using umbraco.cms.businesslogic.web; +using umbraco.cms.presentation.Trees; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// FilteredContentTree for the MultiNodeTreePicker + /// + public class FilteredContentTree : BaseContentTree + { + /// + /// Initializes a new instance of the class. + /// + /// The app. + public FilteredContentTree(string app) + : base(app) + { + } + + /// + /// + /// + private Document m_UserStartNodeDoc; + + /// + /// + /// + private Document m_DefinedStartNodeDoc; + + /// + /// The start node id determined by the defined id and by the user's defined id + /// + private int? m_DeterminedStartNodeId = null; + + /// + /// Returns the Document object of the starting node for the current User. This ensures + /// that the Document object is only instantiated once. + /// + protected Document UserStartNodeDoc + { + get + { + if (m_UserStartNodeDoc == null) + { + if (CurrentUser.StartNodeId <= 0) + { + return new Document(uQuery.RootNodeId, true); + } + m_UserStartNodeDoc = new Document(CurrentUser.StartNodeId); + } + return m_UserStartNodeDoc; + } + } + + /// + /// Returns the Document object of the starting node that is defined in the prevalue editor. This ensures + /// that the Document object is only instantiated once. + /// + protected Document DefinedStartNodeDoc + { + get + { + if (m_DefinedStartNodeDoc == null) + { + var startNodeSelectionType = + this.GetPersistedCookieValue(x => x.MntpGetStartNodeSelectionType(this.GetDataTypeId()), + NodeSelectionType.Picker); + switch (startNodeSelectionType) + { + case NodeSelectionType.Picker: + //if it is a picker, then find the start node id + var definedId = this.GetPersistedCookieValue( + x => x.MntpGetStartNodeId(this.GetDataTypeId()), uQuery.RootNodeId); + //return a document with id -1 (don't set this up as it will exception!) + m_DefinedStartNodeDoc = (definedId > 0) ? new Document(definedId) : new Document(uQuery.RootNodeId, true); + break; + case NodeSelectionType.XPathExpression: + //if it is an expression, then we need to find the start node based on the xpression type, etc... + var expressionType = + this.GetPersistedCookieValue( + x => x.MntpGetStartNodeXPathExpressionType(this.GetDataTypeId()), + XPathExpressionType.Global); + //the default expression should match both schema types + var xpath = + this.GetPersistedCookieValue( + x => x.MntpGetStartNodeXPathExpression(this.GetDataTypeId()), "//*[number(@id)>0]"); + switch (expressionType) + { + case XPathExpressionType.Global: + //if its a global expression, then we need to run the xpath against the entire tree + var nodes = uQuery.GetNodesByXPath(xpath); + //we'll just use the first node found (THERE SHOULD ONLY BE ONE) + m_DefinedStartNodeDoc = nodes.Any() ? new Document(nodes.First().Id) : null; + break; + case XPathExpressionType.FromCurrent: + //if it's a relative query, then it cannot start with //! + if (xpath.StartsWith("/")) + { + throw new InvalidOperationException("A relative xpath query cannot start with a slash"); + } + + //if it's a FromCurrent expression, then we need to run the xpath from this node and below + var currId = + this.GetPersistedCookieValue( + x => x.MntpGetCurrentEditingNode(this.GetDataTypeId()), uQuery.RootNodeId); + var currNode = umbraco.library.GetXmlNodeById(currId.ToString()); + if (currNode.MoveNext()) + { + if (currNode.Current != null) + { + var result = currNode.Current.Select(xpath); + //set it to the first node found (if there is one), otherwise to -1 + if (result.Current != null) + m_DefinedStartNodeDoc = result.MoveNext() ? uQuery.GetDocument(result.Current.GetAttribute("id", string.Empty)) : null; + else + m_DefinedStartNodeDoc = null; + } + else + m_DefinedStartNodeDoc = null; + } + else + { + m_DefinedStartNodeDoc = null; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + + } + return m_DefinedStartNodeDoc; + } + } + + #region Overridden methods + /// + /// Determines the allowed start node id based on the users start node id and the + /// defined start node id in the data type. + /// + public override int StartNodeID + { + get + { + //we only need to determine the id once + if (m_DeterminedStartNodeId.HasValue) + { + return m_DeterminedStartNodeId.Value; + } + + //based on security principles, get the actual start node id + m_DeterminedStartNodeId = this.GetStartNodeId(DefinedStartNodeDoc, UserStartNodeDoc); + + return m_DeterminedStartNodeId.Value; + } + } + + /// + /// Creates the root node. + /// + /// The root node. + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + if (!this.SetNullTreeRootNode(StartNodeID, ref rootNode, app)) + { + rootNode.Action = "javascript:openContent(-1);"; + rootNode.Source = this.GetTreeServiceUrlWithParams(StartNodeID, this.GetDataTypeId()); + if (StartNodeID > 0) + { + var startNode = new Document(StartNodeID); + rootNode.Text = startNode.Text; + rootNode.Icon = startNode.ContentTypeIcon; + } + } + } + + /// + /// Called when [render node]. + /// + /// The x node. + /// The doc. + protected override void OnRenderNode(ref XmlTreeNode xNode, Document doc) + { + base.OnRenderNode(ref xNode, doc); + + var dataTypeId = this.GetDataTypeId(); + var xpath = this.GetXPathFromCookie(dataTypeId); + var xPathType = this.GetXPathFilterTypeFromCookie(dataTypeId); + + // resolves any Umbraco params in the XPath + xpath = uQuery.ResolveXPath(xpath); + + var xDoc = new XmlDocument(); + XmlNode xmlDoc; + if (!doc.Published) + { + xmlDoc = doc.ToPreviewXml(xDoc); + } + else + { + xmlDoc = doc.ToXml(xDoc, false); + } + + var xmlString = "" + xmlDoc.OuterXml + ""; + var xml = XElement.Parse(xmlString); + + xNode.DetermineClickable(xpath, xPathType, xml); + + //ensure that the NodeKey is passed through + xNode.Source = this.GetTreeServiceUrlWithParams(int.Parse(xNode.NodeID), dataTypeId); + } + #endregion + } +} diff --git a/components/editorControls/MultiNodeTreePicker/FilteredMediaTree.cs b/components/editorControls/MultiNodeTreePicker/FilteredMediaTree.cs new file mode 100644 index 0000000000..235ca30493 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/FilteredMediaTree.cs @@ -0,0 +1,146 @@ +using System; +using System.Xml; +using System.Xml.Linq; +using umbraco.cms.businesslogic.media; +using umbraco.cms.presentation.Trees; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// FilteredMediaTree for the MultiNodeTreePicker. + /// + public class FilteredMediaTree : BaseMediaTree + { + /// + /// Initializes a new instance of the class. + /// + /// The app. + public FilteredMediaTree(string app) + : base(app) + { + } + + /// + /// + /// + private Media m_UserStartNodeMedia; + + /// + /// + /// + private Media m_DefinedStartNodeMedia; + + /// + /// Returns the Media object of the starting node for the current User. This ensures + /// that the Media object is only instantiated once. + /// + protected Media UserStartNodeDoc + { + get + { + if (m_UserStartNodeMedia == null) + { + if (CurrentUser.StartMediaId <= 0) + { + return new Media(-1, true); + } + m_UserStartNodeMedia = new Media(CurrentUser.StartMediaId); + } + return m_UserStartNodeMedia; + } + } + + /// + /// Returns the Media object of the starting node that is defined in the prevalue editor. This ensures + /// that the Media object is only instantiated once. + /// + protected Media DefinedStartNodeMedia + { + get + { + if (m_DefinedStartNodeMedia == null) + { + var definedId = this.GetPersistedCookieValue(x => x.MntpGetStartNodeId(this.GetDataTypeId()), -1); + if (definedId <= 0) + { + return new Media(-1, true); + } + m_DefinedStartNodeMedia = new Media(definedId); + } + return m_DefinedStartNodeMedia; + } + } + + /// + /// The start node id determined by the defined id and by the user's defined id + /// + private int? m_DeterminedStartNodeId = null; + + #region Overridden methods + /// + /// Determines the allowed start node id based on the users start node id and the + /// defined start node id in the data type. + /// + public override int StartNodeID + { + get + { + //we only need to determine the id once + if (m_DeterminedStartNodeId.HasValue) + { + return m_DeterminedStartNodeId.Value; + } + + m_DeterminedStartNodeId = this.GetStartNodeId(DefinedStartNodeMedia, UserStartNodeDoc); + + return m_DeterminedStartNodeId.Value; + } + } + + /// + /// Creates the root node. + /// + /// The root node. + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + if (!this.SetNullTreeRootNode(StartNodeID, ref rootNode, app)) + { + rootNode.Action = "javascript:openContent(-1);"; + rootNode.Source = this.GetTreeServiceUrlWithParams(StartNodeID, this.GetDataTypeId()); + if (StartNodeID > 0) + { + var startNode = new Media(StartNodeID); + rootNode.Text = startNode.Text; + rootNode.Icon = startNode.ContentTypeIcon; + } + } + } + + /// + /// Called when [before node render]. + /// + /// The sender. + /// The node. + /// The instance containing the event data. + protected override void OnBeforeNodeRender(ref XmlTree sender, ref XmlTreeNode node, EventArgs e) + { + + var xpath = this.GetXPathFromCookie(this.GetDataTypeId()); + var xPathType = this.GetXPathFilterTypeFromCookie(this.GetDataTypeId()); + var xDoc = new XmlDocument(); + + var xmlNode = umbraco.library.GetMedia(int.Parse(node.NodeID), false).Current.OuterXml; + + var xmlString = "" + xmlNode + ""; + var xml = XElement.Parse(xmlString); + + node.DetermineClickable(xpath, xPathType, xml); + + //ensure that the NodeKey is passed through + node.Source = this.GetTreeServiceUrlWithParams(int.Parse(node.NodeID), this.GetDataTypeId()); + + base.OnBeforeNodeRender(ref sender, ref node, e); + } + #endregion + } +} diff --git a/components/editorControls/MultiNodeTreePicker/HttpCookieExtensions.cs b/components/editorControls/MultiNodeTreePicker/HttpCookieExtensions.cs new file mode 100644 index 0000000000..e03b5c2533 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/HttpCookieExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using System.Web; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + + /// + /// A helper class to store and retrieve cookie values for the MNTP cookie. + /// + /// + /// The cookie is used to persist values from the client to the server since + /// it is much more complicated to try to persist these values between ajax request, + /// given the tree's current architecture. + /// + public static class HttpCookieExtensions + { + + private enum CookieVals + { + /// + /// XPath filter + /// + Xpf, + /// + /// XPath filter type + /// + Xpft, + /// + /// Start node + /// + Sn, + /// + /// Start node xpath expression type + /// + Snxet, + /// + /// Start node select type + /// + Snst, + /// + /// Start node xpath expression + /// + Snxe, + /// + /// Current editing node id + /// + Ceni + } + + + #region Setters + + internal static void MntpAddXPathFilter(this HttpCookie c, int id, string xpath) + { + c[string.Concat(CookieVals.Xpf, "_", id)] = xpath; + } + + internal static void MntpAddXPathFilterType(this HttpCookie c, int id, XPathFilterType type) + { + c[string.Concat(CookieVals.Xpft, "_", id)] = ((int)type).ToString(); + } + + internal static void MntpAddStartNodeId(this HttpCookie c, int id, int startNodeId) + { + c[string.Concat(CookieVals.Sn, "_", id)] = startNodeId.ToString(); + } + + internal static void MntpAddStartNodeXPathExpressionType(this HttpCookie c, int id, XPathExpressionType xPathExpressionType) + { + c[string.Concat(CookieVals.Snxet, "_", id)] = ((int)xPathExpressionType).ToString(); + } + + internal static void MntpAddStartNodeSelectionType(this HttpCookie c, int id, NodeSelectionType startNodeSelectionType) + { + c[string.Concat(CookieVals.Snst, "_", id)] = ((int)startNodeSelectionType).ToString(); + } + + internal static void MntpAddStartNodeXPathExpression(this HttpCookie c, int id, string xPathExpression) + { + c[string.Concat(CookieVals.Snxe, "_", id)] = xPathExpression; + } + + internal static void MntpAddCurrentEditingNode(this HttpCookie c, int id, int currEditingNodeId) + { + c[string.Concat(CookieVals.Ceni, "_", id)] = currEditingNodeId.ToString(); + } + + #endregion + + #region Getters + + internal static string MntpGetXPathFilter(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Xpf, dataTypeId) + ? c.Values[string.Concat(CookieVals.Xpf, "_", dataTypeId)] + : string.Empty; + } + + internal static XPathFilterType MntpGetXPathFilterType(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Xpft, dataTypeId) + ? (XPathFilterType) Enum.ToObject(typeof (XPathFilterType), + int.Parse( + c.Values[string.Concat(CookieVals.Xpft, "_", dataTypeId)])) + : XPathFilterType.Disable; + } + + internal static int MntpGetStartNodeId(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Sn, dataTypeId) + ? int.Parse(c.Values[string.Concat(CookieVals.Sn, "_", dataTypeId)]) + : -1; + } + + internal static XPathExpressionType MntpGetStartNodeXPathExpressionType(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Snxet, dataTypeId) + ? (XPathExpressionType) + Enum.ToObject(typeof(XPathExpressionType), + int.Parse(c.Values[string.Concat(CookieVals.Snxet, "_", dataTypeId)])) + : XPathExpressionType.Global; + } + + internal static NodeSelectionType MntpGetStartNodeSelectionType(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Snst, dataTypeId) + ? (NodeSelectionType) + Enum.ToObject(typeof(NodeSelectionType), + int.Parse(c.Values[string.Concat(CookieVals.Snst, "_", dataTypeId)])) + : NodeSelectionType.Picker; + } + + internal static string MntpGetStartNodeXPathExpression(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Snxe, dataTypeId) + ? c.Values[string.Concat(CookieVals.Snxe, "_", dataTypeId)] + : string.Empty; + } + + internal static int MntpGetCurrentEditingNode(this HttpCookie c, int dataTypeId) + { + return c.ValidateCookieVal(CookieVals.Ceni, dataTypeId) + ? int.Parse(c.Values[string.Concat(CookieVals.Ceni, "_", dataTypeId)]) + : -1; + } + + private static bool ValidateCookieVal(this HttpCookie c, CookieVals val, int dataTypeId) + { + return dataTypeId == 0 ? false : (c.Values.Keys.Cast().Where(x => x == string.Concat(val, "_", dataTypeId)).Any()); + } + + #endregion + } +} diff --git a/components/editorControls/MultiNodeTreePicker/MNTPResources.Designer.cs b/components/editorControls/MultiNodeTreePicker/MNTPResources.Designer.cs new file mode 100644 index 0000000000..e49d834df5 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MNTPResources.Designer.cs @@ -0,0 +1,378 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.488 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class MNTPResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal MNTPResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("uComponents.DataTypes.MultiNodeTreePicker.MNTPResources", typeof(MNTPResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The maximum number of items that are allowed to be selected by a content editor. If an unlimited number of node selections should be allowed, then enter -1 as the value. + /// + public static string Desc_MaxItemsAllowed { + get { + return ResourceManager.GetString("Desc_MaxItemsAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum number of items that are allowed to be selected by a content editor.. + /// + public static string Desc_MinItemsAllowed { + get { + return ResourceManager.GetString("Desc_MinItemsAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nodes can be selected from a parent node (Node Picker) or by an XPath expression. NOTE:When using an XPath expression, only published nodes can be shown.. + /// + public static string Desc_NodeSelectionType { + get { + return ResourceManager.GetString("Desc_NodeSelectionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If enabled and 'media' is selected as the tree type then a thumbnail will be rendered for each item selected.. + /// + public static string Desc_ShowThumbnails { + get { + return ResourceManager.GetString("Desc_ShowThumbnails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If enabled will show an information icon next to each node selected. When the icon is clicked, a tooltip is shown displaying the nodes extended properties.. + /// + public static string Desc_ShowTooltips { + get { + return ResourceManager.GetString("Desc_ShowTooltips", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Define the starting node that should be rendered for the picker. This will ensure that the correct security measures are in place by checking the defined start node ID for the user accessing the picker. In some cases, if the user is not able to view the node, then the picker will render an error message.. + /// + public static string Desc_StartNodeId { + get { + return ResourceManager.GetString("Desc_StartNodeId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can specify to store the data in Umbraco as comma seperated or as XML. By default it is stored as XML which makes it easier to use the saved value in XSLT, however, storing it as comma seperated makes it easier to work with the data using the API such as Node factory.. + /// + public static string Desc_StoreAsComma { + get { + return ResourceManager.GetString("Desc_StoreAsComma", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This XPath expression is used to select a starting node and depends on the XPath expression type chosen (global or from current). IMPORTANT: This XPath expression should be written to match ONE node, if the expression matches more than one node, then the first node matched will be used as the start node.. + /// + public static string Desc_XPathExpression { + get { + return ResourceManager.GetString("Desc_XPathExpression", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The XPath expression that is evaluated to match a start node can be evaluated at a global tree level, or matched from the current node being edited.. + /// + public static string Desc_XPathExpressionType { + get { + return ResourceManager.GetString("Desc_XPathExpressionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An xpath filter to match nodes that will be either enabled or disabled from being clicked (depending on what is selected for the XPath filter type). This XPath filter is for one node only so it should be formatted to select only one node. The XML to XPath against is the same as the Umbraco XML for one node.<br/><br/>Example: /*[name()='myNodeType' or name()='yourNodeType'] <br/><br/>The above would make all nodes of types: myNodeType or yourNodeType not selectable in the tree. + /// + public static string Desc_XPathFilter { + get { + return ResourceManager.GetString("Desc_XPathFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Should the XPath filter match nodes to enable nodes or disable nodes. If Enable is selected, this means that only nodes that match the XPath filter will be allowed to be selected in the tree picker and vise versa for Disabled. + /// + public static string Desc_XPathFilterType { + get { + return ResourceManager.GetString("Desc_XPathFilterType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node Picker. + /// + public static string Item_NodeSelectionType_Picker { + get { + return ResourceManager.GetString("Item_NodeSelectionType_Picker", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XPath Expression. + /// + public static string Item_NodeSelectionType_XPath { + get { + return ResourceManager.GetString("Item_NodeSelectionType_XPath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to From Current. + /// + public static string Item_XPathExpressionType_CurrentNode { + get { + return ResourceManager.GetString("Item_XPathExpressionType_CurrentNode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Global. + /// + public static string Item_XPathExpressionType_Global { + get { + return ResourceManager.GetString("Item_XPathExpressionType_Global", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disable. + /// + public static string Item_XPathMatchType_Disable { + get { + return ResourceManager.GetString("Item_XPathMatchType_Disable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable. + /// + public static string Item_XPathMatchType_Enable { + get { + return ResourceManager.GetString("Item_XPathMatchType_Enable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pixel height of the tree control box. + /// + public static string Lbl_ControlHeight { + get { + return ResourceManager.GetString("Lbl_ControlHeight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum node selections. + /// + public static string Lbl_MaxItemsAllowed { + get { + return ResourceManager.GetString("Lbl_MaxItemsAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimum node selections. + /// + public static string Lbl_MinItemsAllowed { + get { + return ResourceManager.GetString("Lbl_MinItemsAllowed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Node selection type. + /// + public static string Lbl_NodeSelectionType { + get { + return ResourceManager.GetString("Lbl_NodeSelectionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select tree type. + /// + public static string Lbl_SelectTreeType { + get { + return ResourceManager.GetString("Lbl_SelectTreeType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show tooltip for selected item. + /// + public static string Lbl_ShowItemInfoTooltipCheckBox { + get { + return ResourceManager.GetString("Lbl_ShowItemInfoTooltipCheckBox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show thumbnails for media items?. + /// + public static string Lbl_ShowThumbnails { + get { + return ResourceManager.GetString("Lbl_ShowThumbnails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Start node ID. + /// + public static string Lbl_StartNodeId { + get { + return ResourceManager.GetString("Lbl_StartNodeId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data as CSV or XML? . + /// + public static string Lbl_StoreAsComma { + get { + return ResourceManager.GetString("Lbl_StoreAsComma", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XPath expression. + /// + public static string Lbl_XPathExpression { + get { + return ResourceManager.GetString("Lbl_XPathExpression", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XPath type. + /// + public static string Lbl_XPathExpressionType { + get { + return ResourceManager.GetString("Lbl_XPathExpressionType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XPath filter. + /// + public static string Lbl_XPathFilter { + get { + return ResourceManager.GetString("Lbl_XPathFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to XPath filter type. + /// + public static string Lbl_XPathFilterType { + get { + return ResourceManager.GetString("Lbl_XPathFilterType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enter a pixel value between 1 - 999. + /// + public static string Val_ControlHeightMsg { + get { + return ResourceManager.GetString("Val_ControlHeightMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enter only numbers. + /// + public static string Val_MaxItemsMsg { + get { + return ResourceManager.GetString("Val_MaxItemsMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The highlighted Multi Node Tree Picker property requires a minimum node selection of {0}. + /// + public static string Val_MinItemsInvalid { + get { + return ResourceManager.GetString("Val_MinItemsInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enter only numbers. + /// + public static string Val_MinItemsMsg { + get { + return ResourceManager.GetString("Val_MinItemsMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A relative xpath expression cannot start with "/" or "//". + /// + public static string Val_RelativeXpath { + get { + return ResourceManager.GetString("Val_RelativeXpath", resourceCulture); + } + } + } +} diff --git a/components/editorControls/MultiNodeTreePicker/MNTPResources.resx b/components/editorControls/MultiNodeTreePicker/MNTPResources.resx new file mode 100644 index 0000000000..ebb91472b1 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MNTPResources.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The maximum number of items that are allowed to be selected by a content editor. If an unlimited number of node selections should be allowed, then enter -1 as the value + + + The minimum number of items that are allowed to be selected by a content editor. + + + Nodes can be selected from a parent node (Node Picker) or by an XPath expression. NOTE:When using an XPath expression, only published nodes can be shown. + + + If enabled and 'media' is selected as the tree type then a thumbnail will be rendered for each item selected. + + + If enabled will show an information icon next to each node selected. When the icon is clicked, a tooltip is shown displaying the nodes extended properties. + + + Define the starting node that should be rendered for the picker. This will ensure that the correct security measures are in place by checking the defined start node ID for the user accessing the picker. In some cases, if the user is not able to view the node, then the picker will render an error message. + + + You can specify to store the data in Umbraco as comma seperated or as XML. By default it is stored as XML which makes it easier to use the saved value in XSLT, however, storing it as comma seperated makes it easier to work with the data using the API such as Node factory. + + + This XPath expression is used to select a starting node and depends on the XPath expression type chosen (global or from current). IMPORTANT: This XPath expression should be written to match ONE node, if the expression matches more than one node, then the first node matched will be used as the start node. + + + The XPath expression that is evaluated to match a start node can be evaluated at a global tree level, or matched from the current node being edited. + + + An xpath filter to match nodes that will be either enabled or disabled from being clicked (depending on what is selected for the XPath filter type). This XPath filter is for one node only so it should be formatted to select only one node. The XML to XPath against is the same as the Umbraco XML for one node.<br/><br/>Example: /*[name()='myNodeType' or name()='yourNodeType'] <br/><br/>The above would make all nodes of types: myNodeType or yourNodeType not selectable in the tree + + + Should the XPath filter match nodes to enable nodes or disable nodes. If Enable is selected, this means that only nodes that match the XPath filter will be allowed to be selected in the tree picker and vise versa for Disabled + + + Node Picker + + + XPath Expression + + + From Current + + + Global + + + Disable + + + Enable + + + Pixel height of the tree control box + + + Maximum node selections + + + Minimum node selections + + + Node selection type + + + Select tree type + + + Show tooltip for selected item + + + Show thumbnails for media items? + + + Start node ID + + + Data as CSV or XML? + + + XPath expression + + + XPath type + + + XPath filter + + + XPath filter type + + + Please enter a pixel value between 1 - 999 + + + Please enter only numbers + + + The highlighted Multi Node Tree Picker property requires a minimum node selection of {0} + + + Please enter only numbers + + + A relative xpath expression cannot start with "/" or "//" + + \ No newline at end of file diff --git a/components/editorControls/MultiNodeTreePicker/MNTP_DataEditor.cs b/components/editorControls/MultiNodeTreePicker/MNTP_DataEditor.cs new file mode 100644 index 0000000000..8de3df8cb3 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MNTP_DataEditor.cs @@ -0,0 +1,692 @@ +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; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + + /// + /// The user interface to display to the content editor + /// + [ClientDependency( ClientDependencyType.Javascript, "ui/jqueryui.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, + "uComponents.Core", + "DataTypes.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, + "uComponents.Core", + "DataTypes.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.AddAllMNTPClientDependencies(); + + //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) + @"/plugins/MultiNodePicker/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/"; + } +} diff --git a/components/editorControls/MultiNodeTreePicker/MNTP_DataType.cs b/components/editorControls/MultiNodeTreePicker/MNTP_DataType.cs new file mode 100644 index 0000000000..066e868388 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MNTP_DataType.cs @@ -0,0 +1,207 @@ +using System; +using System.Web; +using System.Xml; +using System.Xml.Linq; +using umbraco.cms.businesslogic.datatype; +using umbraco.interfaces; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// Multi-node tree picker data type + /// + public class MNTP_DataType : AbstractDataEditor + { + + /// + /// Initializes a new instance of the class. + /// + public MNTP_DataType() + { + RenderControl = m_Tree; + m_Tree.Init += Tree_Init; + m_Tree.Load += Tree_Load; + DataEditorControl.OnSave += DataEditorControl_OnSave; + + } + + private umbraco.interfaces.IData m_Data; + + /// + /// The internal tree picker control to render + /// + readonly MNTP_DataEditor m_Tree = new MNTP_DataEditor(); + + /// + /// Internal pre value editor to render + /// + MNTP_PrevalueEditor m_PreValues; + + /// + /// + public override Guid Id + { + get + { + return new Guid(DataTypeGuids.MultiNodeTreePickerId); + } + } + + /// + /// + public override string DataTypeName + { + get + { + return "Multi-Node Tree Picker"; + } + } + + /// + /// + public override IData Data + { + get + { + if (this.m_Data == null) + { + m_Data = StoreAsCommaDelimited ? new umbraco.cms.businesslogic.datatype.DefaultData(this) : new XmlData(this); + } + + return m_Data; + } + + } + + /// + /// Value indicating whether to store as comma seperated or Xml + /// + public bool StoreAsCommaDelimited + { + get + { + return ((MNTP_PrevalueEditor)PrevalueEditor).StoreAsCommaDelimited; + } + } + + /// + /// Initialize the tree, here's where we can set some initial properties for the tree + /// + /// + /// + void Tree_Init(object sender, EventArgs e) + { + var preVal = ((MNTP_PrevalueEditor)PrevalueEditor); + m_Tree.TreeToRender = preVal.SelectedTreeType; + m_Tree.XPathFilter = preVal.XPathFilter; + m_Tree.MaxNodeCount = preVal.MaxNodeCount; + m_Tree.ShowToolTips = preVal.ShowToolTip; + m_Tree.XPathFilterMatchType = preVal.XPathFilterMatchType; + m_Tree.StartNodeId = preVal.StartNodeId; + m_Tree.DataTypeDefinitionId = DataTypeDefinitionId; + m_Tree.ShowThumbnailsForMedia = preVal.ShowThumbnailsForMedia; + m_Tree.PropertyName = this.DataTypeName; + m_Tree.StartNodeSelectionType = preVal.StartNodeSelectionType; + m_Tree.StartNodeXPathExpression = preVal.StartNodeXPathExpression; + m_Tree.StartNodeXPathExpressionType = preVal.StartNodeXPathExpressionType; + m_Tree.ControlHeight = preVal.ControlHeight; + m_Tree.MinNodeCount = preVal.MinNodeCount; + + + } + + /// + /// Set the data source for the editor + /// + /// + /// + void Tree_Load(object sender, EventArgs e) + { + if (!m_Tree.Page.IsPostBack) + { + // set the value of the control + if (Data.Value != null && !string.IsNullOrEmpty(Data.Value.ToString())) + { + var strVal = this.Data.Value.ToString(); + + //check how the data is stored + if (StoreAsCommaDelimited) + { + //need to check if it was XML, if so, we'll clear the data (Error checkign) + if (!strVal.StartsWith("<")) + { + m_Tree.SelectedIds = this.Data.Value.ToString().Split(','); + } + else + { + //this must have been saved as XML before, we'll reset it but this will cause a loss of data + m_Tree.SelectedIds = new string[] { }; + } + } + else + { + try + { + var xmlDoc = XDocument.Parse(this.Data.Value.ToString()); + m_Tree.XmlValue = xmlDoc; + } + catch (XmlException) + { + //not a valid xml doc, must have been saved as csv before. + //set to empty val, this will cause a loss of data + m_Tree.XmlValue = null; + } + } + } + } + + } + + /// + /// Handle the saving event, need to give data to Umbraco + /// + /// The instance containing the event data. + void DataEditorControl_OnSave(EventArgs e) + { + string val; + if (StoreAsCommaDelimited) + { + val = string.Join(",", m_Tree.SelectedIds); + } + else + { + val = m_Tree.XmlValue == null ? string.Empty : m_Tree.XmlValue.ToString(); + } + + Data.Value = !string.IsNullOrEmpty(val) ? val : null; + } + + /// + /// return a custom pre value editor + /// + public override IDataPrevalue PrevalueEditor + { + get { return m_PreValues ?? (m_PreValues = new MNTP_PrevalueEditor(this)); } + } + + internal const string PersistenceCookieName = "MultiNodeTreePicker"; + + /// + /// Helper method to ensure the pesistence cookie is cleared. + /// This is used on app startup and after editing the pre-value editor + /// + internal static void ClearCookiePersistence() + { + if (HttpContext.Current == null) + { + return; + } + + if (HttpContext.Current.Response.Cookies[PersistenceCookieName] != null) + { + HttpContext.Current.Response.Cookies[PersistenceCookieName].Expires = DateTime.Now.AddDays(-1); + } + } + + } +} diff --git a/components/editorControls/MultiNodeTreePicker/MNTP_PrevalueEditor.cs b/components/editorControls/MultiNodeTreePicker/MNTP_PrevalueEditor.cs new file mode 100644 index 0000000000..2cb57f40cf --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MNTP_PrevalueEditor.cs @@ -0,0 +1,745 @@ +using System; +using System.Collections; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using System.Web.UI.WebControls; +// using uComponents.Core; +// using uComponents.DataTypes.Shared.Extensions; +using umbraco.cms.businesslogic.datatype; +using umbraco.interfaces; +using umbraco.uicontrols.TreePicker; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// The pre-value editor for the multi node tree picker. + /// + public class MNTP_PrevalueEditor : Control, IDataPrevalue + { + private readonly umbraco.cms.businesslogic.datatype.BaseDataType m_DataType; + private static readonly object Locker = new object(); + private SortedList m_PreValues = null; + + #region Public properties + + /// + /// The chosen tree type to render + /// + public string SelectedTreeType + { + get + { + return GetPreValue(PropertyIndex.TreeType, x => x.Value, "content"); + } + } + + /// + /// An xpath filter to disable nodes to be selectable + /// + public string XPathFilter + { + get + { + return GetPreValue(PropertyIndex.XPathFilter, x => x.Value, string.Empty); + } + } + + /// + /// The number of nodes this picker will support picking + /// + public int MaxNodeCount + { + get + { + return GetPreValue(PropertyIndex.MaxNodeCount, x => + { + var max = -1; + return int.TryParse(x.Value, out max) ? max : -1; + }, -1); + } + } + + /// + /// The minimum number of nodes this picker will support picking + /// + public int MinNodeCount + { + get + { + return GetPreValue(PropertyIndex.MinNodeCount, x => + { + var min = 0; + return int.TryParse(x.Value, out min) ? min : 0; + }, 0); + } + } + + /// + /// A boolean value indicating whether or not to show the informational tool tips + /// + public bool ShowToolTip + { + get + { + return GetPreValue(PropertyIndex.ShowToolTip, x => + { + var show = true; + return bool.TryParse(x.Value, out show) ? show : true; + }, true); + + } + } + + /// + /// Value to check if the data should be stored as CSV or XML + /// + public bool StoreAsCommaDelimited + { + get + { + return GetPreValue(PropertyIndex.StoreAsCommaDelimited, x => + { + var asComma = 0; + return int.TryParse(x.Value, out asComma) ? asComma == 1 : false; + }, false); + + } + } + + /// + /// The XPath expression used when the node type selection is Xpath + /// + public string StartNodeXPathExpression + { + get + { + return GetPreValue(PropertyIndex.StartNodeXPathExpression, x => x.Value, string.Empty); + } + } + + /// + /// The type of xpath expression used for the xpathexpressiontext if using an xpath node selection + /// + public XPathExpressionType StartNodeXPathExpressionType + { + get + { + return GetPreValue(PropertyIndex.StartNodeXPathExpressionType, + x => (XPathExpressionType)Enum.ToObject(typeof(XPathExpressionType), int.Parse(x.Value)), + XPathExpressionType.Global); + } + } + + /// + /// The type of selection type to use for the start node + /// + public NodeSelectionType StartNodeSelectionType + { + get + { + return GetPreValue(PropertyIndex.StartNodeSelectionType, + x => (NodeSelectionType)Enum.ToObject(typeof(NodeSelectionType), int.Parse(x.Value)), + NodeSelectionType.Picker); + } + } + + /// + /// The type of xpath filter applied + /// + public XPathFilterType XPathFilterMatchType + { + get + { + return GetPreValue(PropertyIndex.XPathFilterType, + x => (XPathFilterType)Enum.ToObject(typeof(XPathFilterType), int.Parse(x.Value)), + XPathFilterType.Disable); + } + } + + /// + /// The start node id used when the node selection is a picker + /// + public int StartNodeId + { + get + { + return GetPreValue(PropertyIndex.StartNodeId, x => + { + var max = 0; + return int.TryParse(x.Value, out max) ? max : uQuery.RootNodeId; + }, uQuery.RootNodeId); + } + } + + /// + /// A boolean value indicating whether or not to show the thumbnails for media + /// + public bool ShowThumbnailsForMedia + { + get + { + return GetPreValue(PropertyIndex.ShowThumbnails, x => + { + var show = true; + bool.TryParse(x.Value, out show); + return show; + }, true); + } + } + + /// + /// Returns the control height in pixels + /// + public int ControlHeight + { + get + { + return GetPreValue(PropertyIndex.ControlHeight, x => + { + var max = 0; + return int.TryParse(x.Value, out max) ? max : 200; + }, 200); + } + } + + #endregion + + #region Protected properties + + /// + /// The control height text box + /// + protected TextBox ControlHeightTextBox; + + /// + /// + /// + protected RadioButtonList StartNodeSelectionTypeRadioButtons; + + /// + /// The start node id content picker + /// + protected SimpleContentPicker StartContentNodeIdPicker; + + /// + /// The start node id media picker + /// + protected SimpleMediaPicker StartMediaNodeIdPicker; + + /// + /// XPath expression type radio button list + /// + protected RadioButtonList StartNodeXPathExpressionTypeRadioButtons; + + /// + /// + /// + protected TextBox StartNodeXPathExpressionTextBox; + + /// + /// + /// + protected CheckBox ShowThumbnailsForMediaCheckBox; + + /// + /// + /// + protected DropDownList TreeTypeDropDown; + + /// + /// + /// + protected TextBox XPathFilterTextBox; + + /// + /// Text box for maximum amount of items + /// + protected TextBox MaxItemsTextBox; + + /// + /// Text box for minimum amount of items + /// + protected TextBox MinItemsTextBox; + + /// + /// Minimum items validator + /// + protected RegularExpressionValidator NumbersMinItemsValidator; + + /// + /// Validator for validating relative xpath expressions + /// + protected CustomValidator RelativeXpathValidator; + + /// + /// + /// + protected RegularExpressionValidator NumbersMaxItemsValidator; + + /// + /// + /// + protected RegularExpressionValidator ControlHeightValidatator; + + /// + /// + /// + protected CheckBox ShowItemInfoTooltipCheckBox; + + /// + /// + /// + protected RadioButtonList StoreAsCommaDelimitedRadioButtons; + + /// + /// + /// + protected RadioButtonList XPathFilterTypeRadioButtons; + + #endregion + + /// + /// Initializes a new instance of the class. + /// + /// Type of the data. + public MNTP_PrevalueEditor(umbraco.cms.businesslogic.datatype.BaseDataType dataType) + { + this.m_DataType = dataType; + } + + /// + /// Override on init to ensure child controls + /// + /// An object that contains the event data. + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + this.EnsureChildControls(); + + this.AddResourceToClientDependency("uComponents.DataTypes.Shared.Resources.Styles.PrevalueEditor.css", ClientDependency.Core.ClientDependencyType.Css); + } + + /// + /// Ensures the css to render this control is included. + /// Binds the saved value to the drop down. + /// + /// + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + //add the css required + this.AddCssMNTPClientDependencies(); + + //let view state handle the rest + if (!Page.IsPostBack) + { + TreeTypeDropDown.SelectedValue = SelectedTreeType; + XPathFilterTextBox.Text = XPathFilter; + MaxItemsTextBox.Text = MaxNodeCount.ToString(); + ShowItemInfoTooltipCheckBox.Checked = ShowToolTip; + StoreAsCommaDelimitedRadioButtons.SelectedIndex = StoreAsCommaDelimited ? 1 : 0; + XPathFilterTypeRadioButtons.SelectedIndex = XPathFilterMatchType == XPathFilterType.Disable ? 0 : 1; + ShowThumbnailsForMediaCheckBox.Checked = ShowThumbnailsForMedia; + + StartNodeXPathExpressionTextBox.Text = StartNodeXPathExpression; + StartNodeXPathExpressionTypeRadioButtons.SelectedIndex = StartNodeXPathExpressionType == XPathExpressionType.Global ? 0 : 1; + StartNodeSelectionTypeRadioButtons.SelectedIndex = StartNodeSelectionType == NodeSelectionType.Picker ? 0 : 1; + + switch (SelectedTreeType.ToLower()) + { + case "content": + StartContentNodeIdPicker.Value = StartNodeId.ToString(); + break; + case "media": + default: + StartMediaNodeIdPicker.Value = StartNodeId.ToString(); + break; + } + + ControlHeightTextBox.Text = ControlHeight.ToString(); + MinItemsTextBox.Text = MinNodeCount.ToString(); + } + + + } + + /// + /// Creates child controls for this control + /// + protected override void CreateChildControls() + { + base.CreateChildControls(); + + + TreeTypeDropDown = new DropDownList { ID = "TreeTypeList" }; + TreeTypeDropDown.Items.Add(new ListItem("Content", "content")); + TreeTypeDropDown.Items.Add(new ListItem("Media", "media")); + TreeTypeDropDown.AutoPostBack = true; + AddPreValueRow(MNTPResources.Lbl_SelectTreeType, "", TreeTypeDropDown); + + + StartNodeSelectionTypeRadioButtons = new RadioButtonList { ID = "NodeSelectionTypeRadioButtons" }; + StartNodeSelectionTypeRadioButtons.Items.Add(MNTPResources.Item_NodeSelectionType_Picker); + StartNodeSelectionTypeRadioButtons.Items.Add(new ListItem(MNTPResources.Item_NodeSelectionType_XPath, MNTPResources.Item_NodeSelectionType_XPath.Replace(" ", ""))); + StartNodeSelectionTypeRadioButtons.RepeatDirection = RepeatDirection.Horizontal; + StartNodeSelectionTypeRadioButtons.AutoPostBack = true; + AddPreValueRow(MNTPResources.Lbl_NodeSelectionType, MNTPResources.Desc_NodeSelectionType, StartNodeSelectionTypeRadioButtons); + + + StartContentNodeIdPicker = new SimpleContentPicker { ID = "StartNodeIdTextBox" }; + AddPreValueRow(MNTPResources.Lbl_StartNodeId, MNTPResources.Desc_StartNodeId, StartContentNodeIdPicker); + + StartMediaNodeIdPicker = new SimpleMediaPicker { ID = "StartMediaNodeIdPicker" }; + AddPreValueRow(MNTPResources.Lbl_StartNodeId, MNTPResources.Desc_StartNodeId, StartMediaNodeIdPicker); + + + ShowThumbnailsForMediaCheckBox = new CheckBox { ID = "ShowThumbnailsForMedia" }; + AddPreValueRow(MNTPResources.Lbl_ShowThumbnails, MNTPResources.Desc_ShowThumbnails, ShowThumbnailsForMediaCheckBox); + + StartNodeXPathExpressionTypeRadioButtons = new RadioButtonList { ID = "XPathExpressionTypeRadioButtons" }; + StartNodeXPathExpressionTypeRadioButtons.Items.Add(MNTPResources.Item_XPathExpressionType_Global); + StartNodeXPathExpressionTypeRadioButtons.Items.Add(new ListItem(MNTPResources.Item_XPathExpressionType_CurrentNode, MNTPResources.Item_XPathExpressionType_CurrentNode.Replace(" ", ""))); + StartNodeXPathExpressionTypeRadioButtons.RepeatDirection = RepeatDirection.Horizontal; + AddPreValueRow(MNTPResources.Lbl_XPathExpressionType, MNTPResources.Desc_XPathExpressionType, StartNodeXPathExpressionTypeRadioButtons); + + StartNodeXPathExpressionTextBox = new TextBox { ID = "XPathExpressionTextBox", Width = Unit.Pixel(400) }; + RelativeXpathValidator = new CustomValidator() + { + ID = "RelativeXpathValidator", + ControlToValidate = "XPathExpressionTextBox", + ErrorMessage = MNTPResources.Val_RelativeXpath, + CssClass = "validator" + }; + RelativeXpathValidator.ServerValidate += new ServerValidateEventHandler(RelativeXpathValidator_ServerValidate); + AddPreValueRow(MNTPResources.Lbl_XPathExpression, MNTPResources.Desc_XPathExpression, StartNodeXPathExpressionTextBox, RelativeXpathValidator); + + XPathFilterTypeRadioButtons = new RadioButtonList { ID = "XPathMatchTypeRadioButtons" }; + XPathFilterTypeRadioButtons.Items.Add(MNTPResources.Item_XPathMatchType_Disable); + XPathFilterTypeRadioButtons.Items.Add(MNTPResources.Item_XPathMatchType_Enable); + XPathFilterTypeRadioButtons.RepeatDirection = RepeatDirection.Horizontal; + AddPreValueRow(MNTPResources.Lbl_XPathFilterType, MNTPResources.Desc_XPathFilterType, XPathFilterTypeRadioButtons); + + XPathFilterTextBox = new TextBox { ID = "XPathFilter", Width = Unit.Pixel(400) }; + AddPreValueRow(MNTPResources.Lbl_XPathFilter, MNTPResources.Desc_XPathFilter, XPathFilterTextBox); + + MaxItemsTextBox = new TextBox { ID = "MaxItemsCount", Width = Unit.Pixel(50) }; + NumbersMaxItemsValidator = new RegularExpressionValidator + { + ID = "NumbersMaxItemsValidator", + ControlToValidate = "MaxItemsCount", + CssClass = "validator", + ErrorMessage = MNTPResources.Val_MaxItemsMsg, + ValidationExpression = @"^-{0,1}\d*\.{0,1}\d+$" + }; + AddPreValueRow(MNTPResources.Lbl_MaxItemsAllowed, MNTPResources.Desc_MaxItemsAllowed, MaxItemsTextBox, NumbersMaxItemsValidator); + + MinItemsTextBox = new TextBox { ID = "MinItemsCount", Width = Unit.Pixel(50) }; + NumbersMinItemsValidator = new RegularExpressionValidator + { + ID = "NumbersMinItemsValidator", + ControlToValidate = "MinItemsCount", + CssClass = "validator", + ErrorMessage = MNTPResources.Val_MinItemsMsg, + ValidationExpression = @"^\d{1,3}$" + }; + AddPreValueRow(MNTPResources.Lbl_MinItemsAllowed, MNTPResources.Desc_MinItemsAllowed, MinItemsTextBox, NumbersMinItemsValidator); + + ShowItemInfoTooltipCheckBox = new CheckBox { ID = "ShowItemInfoTooltipCheckBox" }; + AddPreValueRow(MNTPResources.Lbl_ShowItemInfoTooltipCheckBox, MNTPResources.Desc_ShowTooltips, ShowItemInfoTooltipCheckBox); + + StoreAsCommaDelimitedRadioButtons = new RadioButtonList { ID = "StoreAsCommaDelimitedRadioButtons" }; + StoreAsCommaDelimitedRadioButtons.Items.Add("XML"); + StoreAsCommaDelimitedRadioButtons.Items.Add("CSV"); + StoreAsCommaDelimitedRadioButtons.RepeatDirection = RepeatDirection.Horizontal; + AddPreValueRow(MNTPResources.Lbl_StoreAsComma, MNTPResources.Desc_StoreAsComma, StoreAsCommaDelimitedRadioButtons); + + ControlHeightTextBox = new TextBox() { ID = "ControlHeightTextBox", Width = Unit.Pixel(50) }; + ControlHeightValidatator = new RegularExpressionValidator + { + ID = "ControlHeightValidator", + ControlToValidate = "ControlHeightTextBox", + CssClass = "validator", + ErrorMessage = MNTPResources.Val_ControlHeightMsg, + ValidationExpression = @"^\d{1,3}$" + }; + AddPreValueRow(MNTPResources.Lbl_ControlHeight, "", ControlHeightTextBox, ControlHeightValidatator); + } + + void RelativeXpathValidator_ServerValidate(object source, ServerValidateEventArgs args) + { + args.IsValid = true; + if (TreeTypeDropDown.SelectedIndex == 0 && StartNodeXPathExpressionTypeRadioButtons.SelectedIndex == 1 && StartNodeXPathExpressionTextBox.Text.StartsWith("/")) + { + args.IsValid = false; + } + } + + /// + /// Helper method to add a server side pre value row + /// + /// + /// + /// + /// + /// Using server side syntax because of the post backs and because i don't want to manage the view state manually + /// + private void AddPreValueRow(string lbl, string description, params Control[] ctl) + { + var div = new HtmlGenericControl("div"); + div.Attributes.Add("class", "row clearfix"); + var label = new HtmlGenericControl("div"); + label.Attributes.Add("class", "label"); + var span = new HtmlGenericControl("span"); + span.InnerText = lbl; + label.Controls.Add(span); + var field = new HtmlGenericControl("div"); + field.Attributes.Add("class", "field"); + foreach (var c in ctl) + { + field.Controls.Add(c); + } + div.Controls.Add(label); + div.Controls.Add(field); + + if (!string.IsNullOrEmpty(description)) + { + var desc = new HtmlGenericControl("div"); + var descSpan = new HtmlGenericControl("span"); + descSpan.InnerText = description; + desc.Attributes.Add("class", "description"); + desc.Controls.Add(descSpan); + div.Controls.Add(desc); + } + + this.Controls.Add(div); + } + + /// + /// Hides/Shows controls based on the selection of other controls + /// + /// + protected override void OnPreRender(EventArgs e) + { + base.OnPreRender(e); + + //we can only show the node selection type based on content + if (TreeTypeDropDown.SelectedIndex == 0) + { + this.StartNodeSelectionTypeRadioButtons.Parent.Parent.Visible = true; + } + else + { + this.StartNodeSelectionTypeRadioButtons.Parent.Parent.Visible = false; + } + + //if media is selected, or the node type selection is a picker type + if (TreeTypeDropDown.SelectedIndex == 1 || StartNodeSelectionTypeRadioButtons.SelectedIndex == 0) + { + StartNodeXPathExpressionTypeRadioButtons.Parent.Parent.Visible = false; + StartNodeXPathExpressionTextBox.Parent.Parent.Visible = false; + + switch (TreeTypeDropDown.SelectedIndex) + { + case 0: + //content selected + StartContentNodeIdPicker.Parent.Parent.Visible = true; + + StartMediaNodeIdPicker.Parent.Parent.Visible = false; + ShowThumbnailsForMediaCheckBox.Parent.Parent.Visible = false; + break; + case 1: + default: + //media selected: + StartContentNodeIdPicker.Parent.Parent.Visible = false; + + StartMediaNodeIdPicker.Parent.Parent.Visible = true; + ShowThumbnailsForMediaCheckBox.Parent.Parent.Visible = true; + break; + } + + } + else + { + //since it's an xpath expression, not node picker, hide all node pickers + StartContentNodeIdPicker.Parent.Parent.Visible = false; + StartMediaNodeIdPicker.Parent.Parent.Visible = false; + ShowThumbnailsForMediaCheckBox.Parent.Parent.Visible = false; + + StartNodeXPathExpressionTypeRadioButtons.Parent.Parent.Visible = true; + StartNodeXPathExpressionTextBox.Parent.Parent.Visible = true; + } + } + + /// + /// render our own custom markup + /// + /// The object that receives the server control content. + protected override void Render(HtmlTextWriter writer) + { + writer.AddAttribute(HtmlTextWriterAttribute.Class, "uComponents"); + writer.RenderBeginTag(HtmlTextWriterTag.Div); //start 'uComponents' + + base.Render(writer); + + writer.RenderEndTag(); //end 'uComponents' + + } + + /// + /// Lazy loads the prevalues for this data type + /// + /// + private SortedList GetPreValues() + { + if (m_PreValues == null) + { + m_PreValues = PreValues.GetPreValues(m_DataType.DataTypeDefinitionId); + } + return m_PreValues; + } + + #region IDataPrevalue Members + + /// + /// returns this as it's own editor + /// + public Control Editor + { + get { return this; } + } + + /// + /// Saves data to Umbraco + /// + public void Save() + { + if (!Page.IsValid) { return; } + + //it will always be text since people may save a huge amount of selected nodes and serializing to xml could be large. + m_DataType.DBType = umbraco.cms.businesslogic.datatype.DBTypes.Ntext; + + //need to lock this operation since multiple inserts are happening and if 2 threads reach here at the same time, there + //could be issues. + lock (Locker) + { + var vals = GetPreValues(); + + //store the tree type + SavePreValue(PropertyIndex.TreeType, TreeTypeDropDown.SelectedValue, vals); + + //store the xpath + SavePreValue(PropertyIndex.XPathFilter, XPathFilterTextBox.Text, vals); + + //store the max node count + SavePreValue(PropertyIndex.MaxNodeCount, string.IsNullOrEmpty(MaxItemsTextBox.Text) ? "-1" : MaxItemsTextBox.Text, vals); + + //store the 'show tooltips' + SavePreValue(PropertyIndex.ShowToolTip, ShowItemInfoTooltipCheckBox.Checked.ToString(), vals); + + //store the 'as comma' + SavePreValue(PropertyIndex.StoreAsCommaDelimited, StoreAsCommaDelimitedRadioButtons.SelectedIndex.ToString(), vals); + + //the xpath filter type + SavePreValue(PropertyIndex.XPathFilterType, XPathFilterTypeRadioButtons.SelectedIndex.ToString(), vals); + + //based on the media type selected, we need to get the correct start node id + //from the correct control. + var startNodeId = -1; + switch (TreeTypeDropDown.SelectedIndex) + { + case 0: + int.TryParse(StartContentNodeIdPicker.Value, out startNodeId); + break; + case 1: + default: + int.TryParse(StartMediaNodeIdPicker.Value, out startNodeId); + break; + } + + //store the start node id + SavePreValue(PropertyIndex.StartNodeId, startNodeId.ToString(), vals); + + //store the 'show thumbnails' + SavePreValue(PropertyIndex.ShowThumbnails, ShowThumbnailsForMediaCheckBox.Checked.ToString(), vals); + + //store the 'node selection type' + SavePreValue(PropertyIndex.StartNodeSelectionType, StartNodeSelectionTypeRadioButtons.SelectedIndex.ToString(), vals); + + //store the 'xpath expression type' + SavePreValue(PropertyIndex.StartNodeXPathExpressionType, StartNodeXPathExpressionTypeRadioButtons.SelectedIndex.ToString(), vals); + + //save the 'xpath expression' + SavePreValue(PropertyIndex.StartNodeXPathExpression, StartNodeXPathExpressionTextBox.Text, vals); + + //save the control height + SavePreValue(PropertyIndex.ControlHeight, ControlHeightTextBox.Text, vals); + + //save the min amount + SavePreValue(PropertyIndex.MinNodeCount, MinItemsTextBox.Text, vals); + } + + //once everything is saved, clear the cookie vals + MNTP_DataType.ClearCookiePersistence(); + + } + + /// + /// Used to determine the index number of where the property is saved in the pre values repository + /// + private enum PropertyIndex + { + TreeType, + XPathFilter, + MaxNodeCount, + ShowToolTip, + StoreAsCommaDelimited, + XPathFilterType, + StartNodeId, + ShowThumbnails, + StartNodeSelectionType, + StartNodeXPathExpressionType, + StartNodeXPathExpression, + ControlHeight, + MinNodeCount + } + + /// + /// Helper method to save/create pre value values in the db + /// + /// + /// + /// + private void SavePreValue(PropertyIndex propIndex, string value, SortedList currentVals) + { + var index = (int)propIndex; + if (currentVals.Count >= ((int)propIndex + 1)) + { + //update + ((PreValue)currentVals[index]).Value = value; + ((PreValue)currentVals[index]).Save(); + } + else + { + //insert + PreValue.MakeNew(m_DataType.DataTypeDefinitionId, value); + } + } + + /// + /// Generic method to return a strongly typed object from the pre value bucket + /// + /// + /// + /// + /// + /// + private T GetPreValue(PropertyIndex index, Func output, T defaultVal) + { + var vals = GetPreValues(); + return vals.Count >= (int)index + 1 ? output((PreValue)vals[(int)index]) : defaultVal; + } + + #endregion + } +} diff --git a/components/editorControls/MultiNodeTreePicker/MultiNodeTreePickerExtensions.cs b/components/editorControls/MultiNodeTreePicker/MultiNodeTreePickerExtensions.cs new file mode 100644 index 0000000000..4352e7a2ee --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/MultiNodeTreePickerExtensions.cs @@ -0,0 +1,45 @@ +using System.Web.UI; +using umbraco.cms.businesslogic.datatype; + +[assembly: WebResource("umbraco.editorControls.MultiNodeTreePicker.jquery.tooltip.min.js", "application/x-javascript")] +[assembly: WebResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerScripts.js", "application/x-javascript")] +[assembly: WebResource("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerStyles.css", "text/css", PerformSubstitution = true)] + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// Extension methods for this namespace + /// + public static class MultiNodeTreePickerExtensions + { + /// + /// Adds the JS/CSS required for the MultiNodeTreePicker + /// + /// + public static void AddAllMNTPClientDependencies(this Control ctl) + { + //get the urls for the embedded resources + AddCssMNTPClientDependencies(ctl); + AddJsMNTPClientDependencies(ctl); + } + + /// + /// Adds the CSS required for the MultiNodeTreePicker + /// + /// + public static void AddCssMNTPClientDependencies(this Control ctl) + { + ctl.AddResourceToClientDependency("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerStyles.css", ClientDependencyType.Css); + } + + /// + /// Adds the JS required for the MultiNodeTreePicker + /// + /// + public static void AddJsMNTPClientDependencies(this Control ctl) + { + ctl.AddResourceToClientDependency("umbraco.editorControls.MultiNodeTreePicker.MultiNodePickerScripts.js", ClientDependencyType.Javascript); + ctl.AddResourceToClientDependency("umbraco.editorControls.MultiNodeTreePicker.jquery.tooltip.min.js", ClientDependencyType.Javascript); + } + } +} \ No newline at end of file diff --git a/components/editorControls/MultiNodeTreePicker/NodeSelectionType.cs b/components/editorControls/MultiNodeTreePicker/NodeSelectionType.cs new file mode 100644 index 0000000000..76dc493cee --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/NodeSelectionType.cs @@ -0,0 +1,18 @@ +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// + /// + public enum NodeSelectionType + { + /// + /// + /// + Picker, + + /// + /// + /// + XPathExpression + } +} \ No newline at end of file diff --git a/components/editorControls/MultiNodeTreePicker/SelectedItemsTemplate.cs b/components/editorControls/MultiNodeTreePicker/SelectedItemsTemplate.cs new file mode 100644 index 0000000000..9020ebed01 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/SelectedItemsTemplate.cs @@ -0,0 +1,81 @@ +using System.Web; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using System.Web.UI.WebControls; +using umbraco.controls.Images; +using umbraco.IO; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// The item template for the selected items repeater + /// + internal class SelectedItemsTemplate : ITemplate + { + + #region ITemplate Members + + /// + /// Creates the template for the repeater item + /// + /// + public void InstantiateIn(Control container) + { + var itemDiv = new HtmlGenericControl("div"); + itemDiv.ID = "Item"; + itemDiv.Attributes.Add("class", "item"); + + var page = (Page)HttpContext.Current.CurrentHandler; + var imgPreview = (ImageViewer)page.LoadControl( + string.Concat(SystemDirectories.Umbraco, "/controls/Images/ImageViewer.ascx")); + + imgPreview.ID = "ImgPreview"; + imgPreview.Visible = false; //hidden by default + imgPreview.ViewerStyle = ImageViewer.Style.Basic; + itemDiv.Controls.Add(imgPreview); + + var infoBtn = new HtmlAnchor(); + infoBtn.ID = "InfoButton"; + infoBtn.HRef = "javascript:void(0);"; + infoBtn.Attributes.Add("class", "info"); + itemDiv.Controls.Add(infoBtn); + + var innerDiv = new HtmlGenericControl("div"); + innerDiv.ID = "InnerItem"; + innerDiv.Attributes.Add("class", "inner"); + + innerDiv.Controls.Add( + new LiteralControl(@"
    ")); + + var liSelectNode = new HtmlGenericControl("li"); + liSelectNode.Attributes.Add("class", "closed"); + liSelectNode.ID = "SelectedNodeListItem"; + innerDiv.Controls.Add(liSelectNode); + + var selectedNodeLink = new HtmlAnchor(); + selectedNodeLink.ID = "SelectedNodeLink"; + selectedNodeLink.Attributes.Add("class", "sprTree"); + selectedNodeLink.Attributes.Add("title", "Sync tree"); + innerDiv.Controls.Add(selectedNodeLink); + + var selectedNodeText = new Literal(); + selectedNodeText.ID = "SelectedNodeText"; + innerDiv.Controls.Add(selectedNodeText); + + selectedNodeLink.Controls.Add(new LiteralControl("
    ")); + selectedNodeLink.Controls.Add(selectedNodeText); + selectedNodeLink.Controls.Add(new LiteralControl("
    ")); + + liSelectNode.Controls.Add(selectedNodeLink); + + innerDiv.Controls.Add( + new LiteralControl(@"
")); + + itemDiv.Controls.Add(innerDiv); + + container.Controls.Add(itemDiv); + } + + #endregion + } +} diff --git a/components/editorControls/MultiNodeTreePicker/XPathExpressionType.cs b/components/editorControls/MultiNodeTreePicker/XPathExpressionType.cs new file mode 100644 index 0000000000..43b1fb7bc3 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/XPathExpressionType.cs @@ -0,0 +1,18 @@ +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// An enumerator for the XPath expression. + /// + public enum XPathExpressionType + { + /// + /// + /// + Global, + + /// + /// + /// + FromCurrent + } +} \ No newline at end of file diff --git a/components/editorControls/MultiNodeTreePicker/XPathFilterType.cs b/components/editorControls/MultiNodeTreePicker/XPathFilterType.cs new file mode 100644 index 0000000000..4f4a983425 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/XPathFilterType.cs @@ -0,0 +1,18 @@ +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// An enumerator for the XPath filter, for either enable/disable. + /// + public enum XPathFilterType + { + /// + /// Disables the XPath filter. + /// + Disable, + + /// + /// Enables the XPath filter. + /// + Enable + } +} \ No newline at end of file diff --git a/components/editorControls/MultiNodeTreePicker/XmlTreeNodeExtensions.cs b/components/editorControls/MultiNodeTreePicker/XmlTreeNodeExtensions.cs new file mode 100644 index 0000000000..64bd4e4640 --- /dev/null +++ b/components/editorControls/MultiNodeTreePicker/XmlTreeNodeExtensions.cs @@ -0,0 +1,78 @@ +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; +using umbraco.cms.presentation.Trees; + +namespace umbraco.editorControls.MultiNodeTreePicker +{ + /// + /// XmlTreeNode extensions for the MultiNodeTreePicker. + /// + public static class XmlTreeNodeExtensions + { + //public static void DetermineSelected(this XmlTreeNode node) + //{ + //} + + /// + /// Determines if the node should be clickable based on the xpath given + /// + /// The node. + /// The xpath. + /// The type. + /// The XML. + public static void DetermineClickable(this XmlTreeNode node, string xpath, XPathFilterType type, XElement xml) + { + if (!string.IsNullOrEmpty(xpath)) + { + try + { + var matched = xml.XPathSelectElements(xpath); + if (matched.Count() > 0) + { + if (type == XPathFilterType.Disable) + { + //add the non-clickable color to the node + node.Style.AddCustom("uc-treenode-noclick"); + } + else + { + //add the non-clickable color to the node + node.Style.AddCustom("uc-treenode-click"); + } + } + else + { + if (type == XPathFilterType.Disable) + { + //ensure the individual node is the correct color + node.Style.AddCustom("uc-treenode-click"); + } + else + { + //ensure the individual node is the correct color + node.Style.AddCustom("uc-treenode-noclick"); + } + } + } + catch (XPathException) + { + node.Text = "uComponents: XPath Error!"; + } + } + else + { + if (type == XPathFilterType.Disable) + { + //ensure the individual node is the correct color + node.Style.AddCustom("uc-treenode-click"); + } + else + { + //ensure the individual node is the correct color + node.Style.AddCustom("uc-treenode-noclick"); + } + } + } + } +} diff --git a/components/editorControls/umbraco.editorControls.csproj b/components/editorControls/umbraco.editorControls.csproj index e52d320516..77a315e39a 100644 --- a/components/editorControls/umbraco.editorControls.csproj +++ b/components/editorControls/umbraco.editorControls.csproj @@ -133,9 +133,6 @@ System.Windows.Forms - - System.XML - umbraco.businesslogic {E469A9CE-1BEC-423F-AC44-713CD72457EA} @@ -163,6 +160,8 @@ {6EDD2061-82F2-461B-BB6E-879245A832DE} umbraco.controls + + @@ -187,6 +186,7 @@ Code + Code @@ -278,6 +278,24 @@ Code + + + + + + + + + + + + + + + + + + Code @@ -331,6 +349,10 @@ Code + + + + @@ -376,6 +398,14 @@ + + + + + + + + Code @@ -390,6 +420,7 @@ ResXFileCodeGenerator MediaChooserScripts.Designer.cs + textFieldDataEditor.cs Designer @@ -423,6 +454,11 @@ + + + + + diff --git a/umbraco/presentation/umbraco.presentation.csproj b/umbraco/presentation/umbraco.presentation.csproj index 683787a659..2ede531ad4 100644 --- a/umbraco/presentation/umbraco.presentation.csproj +++ b/umbraco/presentation/umbraco.presentation.csproj @@ -514,8 +514,18 @@ ProgressBar.ascx + + ASPXCodeBehind + + + Component + + + + Code + TreeControl.ascx ASPXCodeBehind @@ -1691,6 +1701,7 @@ + @@ -3213,6 +3224,7 @@ welcome.ascx.cs Designer + language.aspx.cs Designer diff --git a/umbraco/presentation/umbraco/controls/Tree/CustomTreeControl.cs b/umbraco/presentation/umbraco/controls/Tree/CustomTreeControl.cs new file mode 100644 index 0000000000..eb284314e9 --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/CustomTreeControl.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using ClientDependency.Core; +using umbraco.controls.Tree; +using umbraco.IO; + +namespace umbraco.controls.Tree +{ + /// + /// A custom tree control that uses a custom web service to return the initial node, this is required + /// due to a bug that exists in Umbraco 4.5.1 tree control/web service. + /// + /// + /// Since we're inheriting from a UserControl and all of the ClientDependency registrations are done inline, we need + /// to re-register the ClientDependencies. + /// + [ClientDependency(10, ClientDependencyType.Css, "Tree/treeIcons.css", "UmbracoClient")] + [ClientDependency(11, ClientDependencyType.Css, "Tree/menuIcons.css", "UmbracoClient")] + [ClientDependency(12, ClientDependencyType.Css, "Tree/Themes/umbraco/style.css", "UmbracoClient")] + [ClientDependency(0, ClientDependencyType.Javascript, "Application/NamespaceManager.js", "UmbracoClient")] + [ClientDependency(ClientDependencyType.Javascript, "Application/UmbracoClientManager.js", "UmbracoClient")] + [ClientDependency(ClientDependencyType.Javascript, "Application/UmbracoApplicationActions.js", "UmbracoClient")] + [ClientDependency(ClientDependencyType.Javascript, "Application/UmbracoUtils.js", "UmbracoClient")] + [ClientDependency(0, ClientDependencyType.Javascript, "ui/jquery.js", "UmbracoClient")] + [ClientDependency(10, ClientDependencyType.Javascript, "Application/JQuery/jquery.metadata.min.js", "UmbracoClient")] + [ClientDependency(11, ClientDependencyType.Javascript, "Tree/jquery.tree.js", "UmbracoClient")] + [ClientDependency(12, ClientDependencyType.Javascript, "Tree/UmbracoContext.js", "UmbracoClient")] + [ClientDependency(12, ClientDependencyType.Javascript, "Tree/jquery.tree.contextmenu.js", "UmbracoClient")] + [ClientDependency(12, ClientDependencyType.Javascript, "Tree/jquery.tree.checkbox.js", "UmbracoClient")] + [ClientDependency(12, ClientDependencyType.Javascript, "Tree/NodeDefinition.js", "UmbracoClient")] + [ClientDependency(13, ClientDependencyType.Javascript, "Tree/UmbracoTree.js", "UmbracoClient")] + public class CustomTreeControl : TreeControl + { + /// + /// Static constructor to ensure that our web service is copied locally + /// + static CustomTreeControl() + { + var servicePath = Path.Combine(SystemDirectories.Umbraco, "plugins", "MultiNodePicker"); + + if (!Directory.Exists(servicePath)) + { + lock (m_Locker) + { + //double check locking + if (!Directory.Exists(servicePath)) + { + //now create our new local web service + var wServiceTxt = CustomTreeServiceResource.CustomTreeService; + var dirMultiNodePicker = new DirectoryInfo(servicePath); + if (!dirMultiNodePicker.Exists) + { + dirMultiNodePicker.Create(); + } + var wServiceFile = new FileInfo(Path.Combine(dirMultiNodePicker.FullName, "CustomTreeService.asmx")); + if (!wServiceFile.Exists) + { + using (var sw = new StreamWriter(wServiceFile.Create())) + { + sw.Write(wServiceTxt); + } + } + } + } + } + } + + private static readonly object m_Locker = new object(); + + /// + /// Ensure child controls are created on init + /// + /// + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + this.EnsureChildControls(); + } + + /// + /// Create the child controls + /// + protected override void CreateChildControls() + { + base.CreateChildControls(); + + TreeContainer = new HtmlGenericControl(); + TreeContainer.TagName = "div"; + TreeContainer.ID = "TreeContainer"; + + this.Controls.Add(TreeContainer); + } + + /// + /// Adds the internal markup to the TreeContainer control + /// + /// + protected override void OnPreRender(EventArgs e) + { + base.OnPreRender(e); + + //add the internal markup to the TreeContainer + /*
*/ + TreeContainer.Controls.Add(new LiteralControl(@"
")); + } + + /// + /// Render out the correct markup for the tree + /// + /// + /// Since we're inheriting from a UserControl, we need to render out the markup manually + /// + /// + protected override void Render(System.Web.UI.HtmlTextWriter writer) + { + //You'll notice that we're replacing the 'serviceUrl' parameter with our own custom web service! + + writer.Write(@" +"); + + //render the controls + TreeContainer.RenderControl(writer); + } + } +} \ No newline at end of file diff --git a/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.asmx b/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.asmx new file mode 100644 index 0000000000..2ddab055f6 --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.asmx @@ -0,0 +1 @@ +<%@ WebService language="C#" class="umbraco.controls.Tree.CustomTreeService" %> \ No newline at end of file diff --git a/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.cs b/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.cs new file mode 100644 index 0000000000..d9a8ae4158 --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/CustomTreeService.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Script.Services; +using System.Web.Services; +using umbraco; +using umbraco.cms.businesslogic; +using umbraco.cms.presentation.Trees; +using umbraco.controls.Tree; + +namespace umbraco.controls.Tree +{ + /// + /// Client side ajax utlities for the tree + /// + [ScriptService] + [WebService] + public class CustomTreeService : WebService + { + /// + /// Returns some info about the node such as path and id + /// + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public NodeInfo GetNodeInfo(int id) + { + Authorize(); + + var node = new CMSNode(id); + return new NodeInfo() + { + Id = node.Id, + Path = node.Path, + PathAsNames = string.Join("->", + GetPathNames(node.Path.Split(',') + .Select(x => int.Parse(x)) + .ToArray())) + }; + } + + /// + /// returns the node names for each id passed in + /// + /// + /// + private string[] GetPathNames(int[] ids) + { + return ids + .Where(x => x != -1) + .Select(x => new CMSNode(x).Text).ToArray(); + } + + /// + /// Returns a key/value object with: json, app, js as the keys + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public Dictionary GetInitAppTreeData(string app, string treeType, bool showContextMenu, bool isDialog, TreeDialogModes dialogMode, string functionToCall, string nodeKey) + { + Authorize(); + + var treeCtl = new TreeControl() + { + ShowContextMenu = showContextMenu, + IsDialog = isDialog, + DialogMode = dialogMode, + App = app, + TreeType = string.IsNullOrEmpty(treeType) ? "" : treeType, //don't set the tree type unless explicitly set + NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey, + //StartNodeID = -1, //TODO: set this based on parameters! + FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall + }; + + var returnVal = new Dictionary(); + + if (string.IsNullOrEmpty(treeType)) + { + //if there's not tree type specified, then render out the tree as per normal with the normal + //way of doing things + returnVal.Add("json", treeCtl.GetJSONInitNode()); + } + else + { + //since 4.5.1 has a bug in it, it ignores if the treeType is specified and will always only render + //the whole APP not just a specific tree. + //this is a work around for this bug until it is fixed (which should be fixed in 4.5.2 + + //get the tree that we need to render + var tree = TreeDefinitionCollection.Instance.FindTree(treeType).CreateInstance(); + tree.ShowContextMenu = showContextMenu; + tree.IsDialog = isDialog; + tree.DialogMode = dialogMode; + tree.NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey; + tree.FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall; + + //now render it's start node + var xTree = new XmlTree(); + + //we're going to hijack the node name here to make it say content/media + var node = tree.RootNode; + if (node.Text.Equals("[FilteredContentTree]")) node.Text = ui.GetText("content"); + else if (node.Text.Equals("[FilteredMediaTree]")) node.Text = ui.GetText("media"); + xTree.Add(node); + + returnVal.Add("json", xTree.ToString()); + } + + returnVal.Add("app", app); + returnVal.Add("js", treeCtl.JSCurrApp); + + return returnVal; + } + + internal static void Authorize() + { + if (!umbraco.BasePages.BasePage.ValidateUserContextID(umbraco.BasePages.BasePage.umbracoUserContextID)) + throw new Exception("Client authorization failed. User is not logged in"); + } + } +} \ No newline at end of file diff --git a/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.Designer.cs b/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.Designer.cs new file mode 100644 index 0000000000..fe84e5292e --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.488 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace umbraco.controls.Tree +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CustomTreeServiceResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CustomTreeServiceResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("uComponents.Core.Shared.Tree.CustomTreeServiceResource", typeof(CustomTreeServiceResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to <%@ WebService language="C#" class="uComponents.Core.Shared.Tree.CustomTreeService" %>. + /// + internal static string CustomTreeService { + get { + return ResourceManager.GetString("CustomTreeService", resourceCulture); + } + } + } +} diff --git a/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.resx b/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.resx new file mode 100644 index 0000000000..ff813c630a --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/CustomTreeServiceResource.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + customtreeservice.asmx;System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + \ No newline at end of file diff --git a/umbraco/presentation/umbraco/controls/Tree/NodeInfo.cs b/umbraco/presentation/umbraco/controls/Tree/NodeInfo.cs new file mode 100644 index 0000000000..48f46e30db --- /dev/null +++ b/umbraco/presentation/umbraco/controls/Tree/NodeInfo.cs @@ -0,0 +1,32 @@ +namespace umbraco.controls.Tree +{ + /// + /// Simple data object to hold information about a node + /// + public class NodeInfo + { + /// + /// Gets or sets the id. + /// + /// The id. + public int Id { get; set; } + + /// + /// Gets or sets the path. + /// + /// The path. + public string Path { get; set; } + + /// + /// Gets or sets the path as names. + /// + /// The path as names. + public string PathAsNames { get; set; } + + /// + /// Gets or sets the type of the node. + /// + /// The type of the node. + public string NodeType { get; set; } + } +} \ No newline at end of file