diff --git a/umbraco/presentation.ClientDependency/ClientDependencyAttribute.cs b/umbraco/presentation.ClientDependency/ClientDependencyAttribute.cs index 419d90fb31..dabb858417 100644 --- a/umbraco/presentation.ClientDependency/ClientDependencyAttribute.cs +++ b/umbraco/presentation.ClientDependency/ClientDependencyAttribute.cs @@ -15,6 +15,7 @@ namespace umbraco.presentation.ClientDependency public ClientDependencyAttribute() { Priority = DefaultPriority; + DoNotOptimize = false; } /// @@ -26,6 +27,16 @@ namespace umbraco.presentation.ClientDependency /// protected const int DefaultPriority = 100; + /// + /// If set to true, this file will not be compressed, combined, etc... + /// it will be rendered out as is. + /// + /// + /// Useful for debugging dodgy scripts. + /// Default is false. + /// + public bool DoNotOptimize { get; set; } + /// /// If dependencies have a composite group name specified, the system will combine all dependency /// file contents under the one group name and GZIP the output to output cache. diff --git a/umbraco/presentation.ClientDependency/ClientDependencyHelper.cs b/umbraco/presentation.ClientDependency/ClientDependencyHelper.cs index 3c4f598560..6da31df8ff 100644 --- a/umbraco/presentation.ClientDependency/ClientDependencyHelper.cs +++ b/umbraco/presentation.ClientDependency/ClientDependencyHelper.cs @@ -15,6 +15,7 @@ namespace umbraco.presentation.ClientDependency private static ClientDependencyProvider m_Provider = null; private static ClientDependencyProviderCollection m_Providers = null; + private static List m_Extensions; private static object m_Lock = new object(); public static ClientDependencyProvider DefaultProvider @@ -34,6 +35,26 @@ namespace umbraco.presentation.ClientDependency } } + /// + /// The file extensions of Client Dependencies that are file based as opposed to request based. + /// Any file that doesn't have the extensions listed here will be request based, request based is + /// more overhead for the server to process. + /// + /// + /// A request based JavaScript file may be a .ashx that dynamically creates JavaScript server side. + /// + /// + /// If this is not explicitly set, then the extensions 'js' and 'css' are the defaults. + /// + public static List FileBasedDependencyExtensionList + { + get + { + LoadProviders(); + return m_Extensions; + } + } + private static void LoadProviders() { if (m_Provider == null) @@ -43,7 +64,7 @@ namespace umbraco.presentation.ClientDependency // Do this again to make sure _provider is still null if (m_Provider == null) { - ClientDependencySection section = (ClientDependencySection)WebConfigurationManager.GetSection("system.web/imageService"); + ClientDependencySection section = (ClientDependencySection)WebConfigurationManager.GetSection("system.web/clientDependency"); m_Providers = new ClientDependencyProviderCollection(); @@ -55,17 +76,23 @@ namespace umbraco.presentation.ClientDependency ProvidersHelper.InstantiateProviders(section.Providers, m_Providers, typeof(ClientDependencyProvider)); m_Provider = m_Providers[section.DefaultProvider]; if (m_Provider == null) - throw new ProviderException("Unable to load default ImageProvider"); + throw new ProviderException("Unable to load default ClientDependency provider"); } else { + //get the default settings + section = new ClientDependencySection(); + m_Extensions = section.FileBasedDependencyExtensionList; + PageHeaderProvider php = new PageHeaderProvider(); php.Initialize(PageHeaderProvider.DefaultName, null); m_Providers.Add(php); ClientSideRegistrationProvider csrp = new ClientSideRegistrationProvider(); csrp.Initialize(ClientSideRegistrationProvider.DefaultName, null); m_Providers.Add(csrp); - m_Provider = m_Providers[PageHeaderProvider.DefaultName]; + + //set the default + m_Provider = m_Providers[section.DefaultProvider]; } } } diff --git a/umbraco/presentation.ClientDependency/ClientDependencyInclude.cs b/umbraco/presentation.ClientDependency/ClientDependencyInclude.cs index f3b6fd948a..76aba91f6c 100644 --- a/umbraco/presentation.ClientDependency/ClientDependencyInclude.cs +++ b/umbraco/presentation.ClientDependency/ClientDependencyInclude.cs @@ -12,6 +12,7 @@ namespace umbraco.presentation.ClientDependency { DependencyType = ClientDependencyType.Javascript; Priority = DefaultPriority; + DoNotOptimize = false; } /// @@ -23,6 +24,16 @@ namespace umbraco.presentation.ClientDependency /// protected const int DefaultPriority = 100; + /// + /// If set to true, this file will not be compressed, combined, etc... + /// it will be rendered out as is. + /// + /// + /// Useful for debugging dodgy scripts. + /// Default is false. + /// + public bool DoNotOptimize { get; set; } + public ClientDependencyType DependencyType { get; set; } public string FilePath { get; set; } public string PathNameAlias { get; set; } diff --git a/umbraco/presentation.ClientDependency/CompositeDependencyHandler.cs b/umbraco/presentation.ClientDependency/CompositeDependencyHandler.cs index d3772a857b..96c791e0f2 100644 --- a/umbraco/presentation.ClientDependency/CompositeDependencyHandler.cs +++ b/umbraco/presentation.ClientDependency/CompositeDependencyHandler.cs @@ -5,6 +5,7 @@ using System.Web; using System.Reflection; using System.IO; using System.IO.Compression; +using System.Net; namespace umbraco.presentation.ClientDependency { @@ -63,7 +64,7 @@ namespace umbraco.presentation.ClientDependency if (string.IsNullOrEmpty(fileset)) throw new ArgumentException("Must specify a fileset in the request"); - byte[] fileBytes = CombineFiles(fileset, context); + byte[] fileBytes = CombineFiles(fileset, context, type); byte[] outputBytes = CompressBytes(context, fileBytes); SetCaching(context); @@ -77,22 +78,61 @@ namespace umbraco.presentation.ClientDependency /// /// /// - private byte[] CombineFiles(string fileList, HttpContext context) + private byte[] CombineFiles(string fileList, HttpContext context, ClientDependencyType type) { MemoryStream ms = new MemoryStream(5000); //get the file list, and write the contents of each file to the output stream. string[] strFiles = DecodeFrom64(fileList).Split(';'); - StreamWriter sw = new StreamWriter(ms); + StreamWriter sw = new StreamWriter(ms); foreach (string s in strFiles) { if (!string.IsNullOrEmpty(s)) { - FileInfo fi = new FileInfo(context.Server.MapPath(s)); - if (!fi.Exists) - throw new NullReferenceException("File could not be found: " + fi.FullName); - string fileContents = File.ReadAllText(fi.FullName); - sw.WriteLine(fileContents); + try + { + FileInfo fi = new FileInfo(context.Server.MapPath(s)); + if (ClientDependencyHelper.FileBasedDependencyExtensionList.Contains(fi.Extension.ToLower().Replace(".", ""))) + { + //if the file doesn't exist, then we'll assume it is a URI external request + if (!fi.Exists) + { + WriteRequestToStream(ref sw, s); + } + else + { + //if it is a file based dependency then read it + string fileContents = File.ReadAllText(fi.FullName); + sw.WriteLine(fileContents); + } + } + else + { + //if it's not a file based dependency, try to get the request output. + WriteRequestToStream(ref sw, s); + } + } + catch (Exception ex) + { + Type exType = ex.GetType(); + if (exType.Equals(typeof(NotSupportedException)) || exType.Equals(typeof(ArgumentException))) + { + //could not parse the string into a fileinfo, so we assume it is a URI + WriteRequestToStream(ref sw, s); + } + else + { + //if this fails, log the exception in trace, but continue + HttpContext.Current.Trace.Warn("ClientDependency", "Could not load file contents from " + s, ex); + System.Diagnostics.Debug.Assert(false, "Could not load file contents from " + s, ex.Message); + } + } } + + if (type == ClientDependencyType.Javascript) + { + sw.Write(";;;"); //write semicolons in case the js isn't formatted correctly. This also helps for debugging. + } + } sw.Flush(); byte[] outputBytes = ms.ToArray(); @@ -101,6 +141,74 @@ namespace umbraco.presentation.ClientDependency return outputBytes; } + /// + /// Writes the output of an external request to the stream. Returns true/false if succesful or not. + /// + /// + /// + /// + private bool WriteRequestToStream(ref StreamWriter sw, string url) + { + string requestOutput; + bool rVal = false; + rVal = TryReadUri(url, out requestOutput); + if (rVal) + { + //write the contents of the external request. + sw.WriteLine(requestOutput); + } + return rVal; + } + + /// + /// Tries to convert the url to a uri, then read the request into a string and return it. + /// This takes into account relative vs absolute URI's + /// + /// + /// + /// + private bool TryReadUri(string url, out string requestContents) + { + Uri uri; + if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)) + { + if (uri.IsAbsoluteUri) + { + WebClient client = new WebClient(); + try + { + requestContents = client.DownloadString(uri.AbsoluteUri); + return true; + } + catch (Exception ex) + { + HttpContext.Current.Trace.Warn("ClientDependency", "Could not load file contents from " + url, ex); + System.Diagnostics.Debug.Assert(false, "Could not load file contents from " + url, ex.Message); + } + } + else + { + //its a relative path so use the execute method + StringWriter sw = new StringWriter(); + try + { + HttpContext.Current.Server.Execute(url, sw); + requestContents = sw.ToString(); + sw.Close(); + return true; + } + catch (Exception ex) + { + HttpContext.Current.Trace.Warn("ClientDependency", "Could not load file contents from " + url, ex); + System.Diagnostics.Debug.Assert(false, "Could not load file contents from " + url, ex.Message); + } + } + + } + requestContents = ""; + return false; + } + /// /// Sets the output cache parameters and also the client side caching parameters /// @@ -112,7 +220,7 @@ namespace umbraco.presentation.ClientDependency TimeSpan duration = TimeSpan.FromDays(10); HttpCachePolicy cache = context.Response.Cache; cache.SetCacheability(HttpCacheability.Public); - cache.SetExpires(DateTime.Now.Add(duration)); + cache.SetExpires(DateTime.Now.Add(duration)); cache.SetMaxAge(duration); cache.SetValidUntilExpires(true); cache.SetLastModified(DateTime.Now); @@ -133,7 +241,7 @@ namespace umbraco.presentation.ClientDependency /// Compresses the bytes if the browser supports it /// private byte[] CompressBytes(HttpContext context, byte[] fileBytes) - { + { string acceptEncoding = context.Request.Headers["Accept-Encoding"]; if (!string.IsNullOrEmpty(acceptEncoding)) { @@ -141,18 +249,21 @@ namespace umbraco.presentation.ClientDependency Stream compressedStream = null; acceptEncoding = acceptEncoding.ToLowerInvariant(); bool isCompressed = false; - if (acceptEncoding.Contains("gzip")) - { - context.Response.AddHeader("Content-encoding", "gzip"); - compressedStream = new GZipStream(ms, CompressionMode.Compress, true); - isCompressed = true; - } - else if (acceptEncoding.Contains("deflate")) + + //deflate is faster in .Net according to Mads Kristensen (blogengine.net) + if (acceptEncoding.Contains("deflate")) { context.Response.AddHeader("Content-encoding", "deflate"); compressedStream = new DeflateStream(ms, CompressionMode.Compress, true); isCompressed = true; } + else if (acceptEncoding.Contains("gzip")) + { + context.Response.AddHeader("Content-encoding", "gzip"); + compressedStream = new GZipStream(ms, CompressionMode.Compress, true); + isCompressed = true; + } + if (isCompressed) { //write the bytes to the compressed stream diff --git a/umbraco/presentation.ClientDependency/IClientDependencyFile.cs b/umbraco/presentation.ClientDependency/IClientDependencyFile.cs index 6cf6751768..4d24fc4122 100644 --- a/umbraco/presentation.ClientDependency/IClientDependencyFile.cs +++ b/umbraco/presentation.ClientDependency/IClientDependencyFile.cs @@ -12,5 +12,6 @@ namespace umbraco.presentation.ClientDependency int Priority { get; set; } //string CompositeGroupName { get; set; } string PathNameAlias { get; set; } + bool DoNotOptimize { get; set; } } } diff --git a/umbraco/presentation.ClientDependency/Providers/ClientDependencyProvider.cs b/umbraco/presentation.ClientDependency/Providers/ClientDependencyProvider.cs index 3c73de75d0..5ee86b861a 100644 --- a/umbraco/presentation.ClientDependency/Providers/ClientDependencyProvider.cs +++ b/umbraco/presentation.ClientDependency/Providers/ClientDependencyProvider.cs @@ -4,6 +4,7 @@ using System.Text; using System.Web.UI; using System.Configuration.Provider; using System.Web; +using System.Linq; namespace umbraco.presentation.ClientDependency.Providers { @@ -20,6 +21,8 @@ namespace umbraco.presentation.ClientDependency.Providers protected abstract void RegisterJsFiles(List jsDependencies); protected abstract void RegisterCssFiles(List cssDependencies); + protected abstract void ProcessSingleJsFile(string js); + protected abstract void ProcessSingleCssFile(string css); public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { @@ -98,31 +101,44 @@ namespace umbraco.presentation.ClientDependency.Providers //} /// - /// Returns a full url with the encoded query strings for the handler which will process the composite group. + /// Returns a list of urls. The array will consist of only one entry if + /// none of the dependencies are tagged as DoNotOptimize, otherwise, if any of them are, + /// this will return the path to the file. + /// + /// For the optimized files, the full url with the encoded query strings for the handler which will process the composite list + /// of dependencies. The handler will compbine, compress, minify (if JS), and output cache the results + /// based on a hash key of the base64 encoded string. /// /// /// + /// + /// If the DoNotOptimize setting has been set for any of the dependencies in the list, then this will ignore them. + /// /// - //private string ProcessCompositeGroup(List dependencies, string groupName, ClientDependencyType type) - //{ - // string handler = "{0}?s={1}&t={2}"; - // StringBuilder files = new StringBuilder(); - // List byGroup = dependencies.FindAll( - // delegate(IClientDependencyFile a) - // { - // return a.CompositeGroupName == groupName; - // } - // ); - // byGroup.Sort((a, b) => a.Priority.CompareTo(b.Priority)); - // foreach (IClientDependencyFile a in byGroup) - // { - // files.Append(a.FilePath + ";"); - // } - // string url = string.Format(handler, CompositeDependencyHandler.HandlerFileName, HttpContext.Current.Server.UrlEncode(EncodeTo64(files.ToString())), type.ToString()); - // if (url.Length > CompositeDependencyHandler.MaxHandlerUrlLength) - // throw new ArgumentOutOfRangeException("The number of files in the composite group " + groupName + " creates a url handler address that exceeds the CompositeDependencyHandler MaxHandlerUrlLength. Reducing the amount of files in this composite group should fix the issue"); - // return url; - //} + protected List ProcessCompositeList(List dependencies, ClientDependencyType type) + { + List rVal = new List(); + + //build the combined composite list url + string handler = "{0}?s={1}&t={2}"; + StringBuilder files = new StringBuilder(); + foreach (IClientDependencyFile a in dependencies.Where(x => !x.DoNotOptimize)) + { + files.Append(a.FilePath + ";"); + } + string combinedurl = string.Format(handler, CompositeDependencyHandler.HandlerFileName, HttpContext.Current.Server.UrlEncode(EncodeTo64(files.ToString())), type.ToString()); + rVal.Add(combinedurl); + + //add any urls that are not to be optimized + foreach (IClientDependencyFile a in dependencies.Where(x => x.DoNotOptimize)) + { + rVal.Add(a.FilePath); + } + + //if (url.Length > CompositeDependencyHandler.MaxHandlerUrlLength) + // throw new ArgumentOutOfRangeException("The number of files in the composite group " + groupName + " creates a url handler address that exceeds the CompositeDependencyHandler MaxHandlerUrlLength. Reducing the amount of files in this composite group should fix the issue"); + return rVal; + } private string EncodeTo64(string toEncode) { @@ -164,21 +180,21 @@ namespace umbraco.presentation.ClientDependency.Providers } } - internal class SimpleDependencyFile : IClientDependencyFile - { - public SimpleDependencyFile(string filePath, ClientDependencyType type) - { - FilePath = filePath; - DependencyType = type; - } + //internal class SimpleDependencyFile : IClientDependencyFile + //{ + // public SimpleDependencyFile(string filePath, ClientDependencyType type) + // { + // FilePath = filePath; + // DependencyType = type; + // } - public string FilePath{get;set;} - public ClientDependencyType DependencyType { get; set; } - public string InvokeJavascriptMethodOnLoad { get; set; } - public int Priority { get; set; } - //public string CompositeGroupName { get; set; } - public string PathNameAlias { get; set; } - } + // public string FilePath{get;set;} + // public ClientDependencyType DependencyType { get; set; } + // public string InvokeJavascriptMethodOnLoad { get; set; } + // public int Priority { get; set; } + // //public string CompositeGroupName { get; set; } + // public string PathNameAlias { get; set; } + //} } } diff --git a/umbraco/presentation.ClientDependency/Providers/ClientDependencySection.cs b/umbraco/presentation.ClientDependency/Providers/ClientDependencySection.cs index 7bae30f385..5996814de3 100644 --- a/umbraco/presentation.ClientDependency/Providers/ClientDependencySection.cs +++ b/umbraco/presentation.ClientDependency/Providers/ClientDependencySection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Configuration; +using System.Linq; namespace umbraco.presentation.ClientDependency.Providers { @@ -20,6 +21,41 @@ namespace umbraco.presentation.ClientDependency.Providers get { return (string)base["defaultProvider"]; } set { base["defaultProvider"] = value; } } + + + /// + /// The configuration section to set the FileBasedDependencyExtensionList. This is a comma separated list. + /// + /// + /// If this is not explicitly set, then the extensions 'js' and 'css' are the defaults. + /// + [ConfigurationProperty("fileDependencyExtensions", DefaultValue = "js,css")] + protected string FileBasedDepdendenyExtensions + { + get { return (string)base["fileDependencyExtensions"]; } + set { base["fileDependencyExtensions"] = value; } + } + + /// + /// The file extensions of Client Dependencies that are file based as opposed to request based. + /// Any file that doesn't have the extensions listed here will be request based, request based is + /// more overhead for the server to process. + /// + /// + /// A request based JavaScript file may be a .ashx that dynamically creates JavaScript server side. + /// + /// + /// If this is not explicitly set, then the extensions 'js' and 'css' are the defaults. + /// + public List FileBasedDependencyExtensionList + { + get + { + return FileBasedDepdendenyExtensions.Split(',') + .Select(x => x.Trim().ToLower()) + .ToList(); + } + } } } diff --git a/umbraco/presentation.ClientDependency/Providers/ClientSideRegistrationProvider.cs b/umbraco/presentation.ClientDependency/Providers/ClientSideRegistrationProvider.cs index b8c65ff4a5..c6074607ce 100644 --- a/umbraco/presentation.ClientDependency/Providers/ClientSideRegistrationProvider.cs +++ b/umbraco/presentation.ClientDependency/Providers/ClientSideRegistrationProvider.cs @@ -43,6 +43,11 @@ namespace umbraco.presentation.ClientDependency.Providers dependencyCalls.ToString(), true); } + protected override void ProcessSingleJsFile(string js) + { + throw new NotImplementedException(); + } + protected override void RegisterCssFiles(List cssDependencies) { if (cssDependencies.Count == 0) @@ -62,6 +67,11 @@ namespace umbraco.presentation.ClientDependency.Providers dependencyCalls.ToString(), true); } + protected override void ProcessSingleCssFile(string css) + { + throw new NotImplementedException(); + } + private void RegisterDependencyLoader() { // register loader script diff --git a/umbraco/presentation.ClientDependency/Providers/PageHeaderProvider.cs b/umbraco/presentation.ClientDependency/Providers/PageHeaderProvider.cs index a38cba0ff5..39e802de69 100644 --- a/umbraco/presentation.ClientDependency/Providers/PageHeaderProvider.cs +++ b/umbraco/presentation.ClientDependency/Providers/PageHeaderProvider.cs @@ -9,6 +9,8 @@ namespace umbraco.presentation.ClientDependency.Providers { public const string DefaultName = "PageHeaderProvider"; + public const string ScriptEmbed = ""; + public const string CssEmbed = ""; public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { @@ -21,22 +23,55 @@ namespace umbraco.presentation.ClientDependency.Providers protected override void RegisterJsFiles(List jsDependencies) { - string js = ""; - foreach (IClientDependencyFile dependency in jsDependencies) + + if (IsDebugMode) { - DependantControl.Page.Trace.Write("ClientDependency", string.Format("Registering: {0}", dependency.FilePath)); - DependantControl.Page.Header.Controls.Add(new LiteralControl(string.Format(js, dependency.FilePath))); + foreach (IClientDependencyFile dependency in jsDependencies) + { + ProcessSingleJsFile(dependency.FilePath); + } } + else + { + List jsList = ProcessCompositeList(jsDependencies, ClientDependencyType.Javascript); + DependantControl.Page.Trace.Write("ClientDependency", string.Format("Processed composite list: {0}", jsList[0])); + foreach (string js in jsList) + { + ProcessSingleJsFile(js); + } + } + } + + protected override void ProcessSingleJsFile(string js) + { + DependantControl.Page.Trace.Write("ClientDependency", string.Format("Registering: {0}", js)); + DependantControl.Page.Header.Controls.Add(new LiteralControl(string.Format(ScriptEmbed, js))); } protected override void RegisterCssFiles(List cssDependencies) { - string css = ""; - foreach (IClientDependencyFile dependency in cssDependencies) + if (IsDebugMode) { - DependantControl.Page.Trace.Write("ClientDependency", string.Format("Registering: {0}", dependency.FilePath)); - DependantControl.Page.Header.Controls.Add(new LiteralControl(string.Format(css, dependency.FilePath))); + foreach (IClientDependencyFile dependency in cssDependencies) + { + ProcessSingleCssFile(dependency.FilePath); + } } + else + { + List cssList = ProcessCompositeList(cssDependencies, ClientDependencyType.Css); + DependantControl.Page.Trace.Write("ClientDependency", string.Format("Processed composite list: {0}", cssList[0])); + foreach (string css in cssList) + { + ProcessSingleCssFile(css); + } + } + } + + protected override void ProcessSingleCssFile(string css) + { + DependantControl.Page.Trace.Write("ClientDependency", string.Format("Registering: {0}", css)); + DependantControl.Page.Header.Controls.Add(new LiteralControl(string.Format(CssEmbed, css))); } } } diff --git a/umbraco/presentation/umbraco.presentation.csproj b/umbraco/presentation/umbraco.presentation.csproj index 03c99523c3..5c5dd56b2b 100644 --- a/umbraco/presentation/umbraco.presentation.csproj +++ b/umbraco/presentation/umbraco.presentation.csproj @@ -1806,6 +1806,7 @@ + diff --git a/umbraco/presentation/umbraco/Default.aspx.cs b/umbraco/presentation/umbraco/Default.aspx.cs index 0a0d0deef9..84b7f2f76b 100644 --- a/umbraco/presentation/umbraco/Default.aspx.cs +++ b/umbraco/presentation/umbraco/Default.aspx.cs @@ -18,7 +18,7 @@ namespace umbraco { protected void Page_Load(object sender, System.EventArgs e) { - + Server.Transfer("umbraco.aspx"); // Put user code to initialize the page here } diff --git a/umbraco/presentation/umbraco/controls/TreeControl.ascx b/umbraco/presentation/umbraco/controls/TreeControl.ascx index 0656df369c..3de24ace00 100644 --- a/umbraco/presentation/umbraco/controls/TreeControl.ascx +++ b/umbraco/presentation/umbraco/controls/TreeControl.ascx @@ -12,7 +12,7 @@ - + "); } + _this._initNode = eval(msg.json); _this._tree = $.tree_create(); _this._tree.init(_this._container, _this._getInitOptions()); _this._container.show(); _this._loadChildNodes(_this._container.find("li:first"), null); _this._currentAJAXRequest = false; $(_this).trigger("rebuiltTree", [msg.app]); + }, error: function(e) { _this._debug("rebuildTree: AJAX error occurred"); _this._currentAJAXRequest = false; $(_this).trigger("ajaxError", [{ msg: "rebuildTree"}]); } + }); + }, saveTreeState: function(appAlias) { + this._debug("saveTreeState: " + appAlias + " : ajax request? " + this._currentAJAXRequest); if (this._currentAJAXRequest) { this._container.data("tree_" + appAlias, null); return; } + var treeData = this._tree.getJSON(null, ["id", "umb:type", "class"], ["umb:nodedata", "href", "class", "style"]); this._updateJSONNodeState(treeData); this._container.data("tree_" + appAlias, { selected: this._tree.selected, d: treeData }); + }, _updateJSONNodeState: function(obj) { + var node = $("li[id='" + obj.attributes.id + "']").filter(function() { return ($(this).attr("umb:type") == obj.attributes["umb:type"]); }); var c = node.attr("class"); if (c != null) { var state = c.indexOf("open") > -1 ? "open" : c.indexOf("closed") > -1 ? "closed" : null; if (state != null) obj.state = state; } + if (obj.children != null) { for (var x in obj.children) { this._updateJSONNodeState(obj.children[x]); } } + }, syncTree: function(path, forceReload) { this._debug("syncTree: " + path + ", " + forceReload); this._syncTree.call(this, path, forceReload, null, null); }, childNodeCreated: function() { + this._debug("childNodeCreated"); var childrenIds = new Array(); this._actionNode.jsNode.find("ul > li").each(function() { childrenIds.push($(this).attr("id")); }); var _this = this; var currId = this._actionNode.nodeId; this.reloadActionNode(true, false, function(success) { + if (success && childrenIds.length > 0) { + var found = false; var actionNode = _this.findNode(currId); if (actionNode) { actionNode.find("ul > li").each(function() { if ($.inArray($(this).attr("id"), childrenIds) == -1) { found = $(this); } }); } + if (found) { _this._debug("childNodeCreated: selecting new child node: " + found.attr("id")); _this.selectNode(found, true, true); $(_this).trigger("newChildNodeFound", [found]); return; } + } + _this._debug("childNodeCreated: could not select new child!"); + }); + }, moveNode: function(nodeId, parentPath) { this._debug("moveNode"); var old = this.findNode(nodeId); if (old) old.remove(); var newPath = parentPath + "," + nodeId; var _this = this; var foundHandler = function(EV, node) { _this.removeEventHandler("syncFound", foundHandler); _this.selectNode(node, false, true); $(_this).trigger("nodeMoved", [node]); }; this.addEventHandler("syncFound", foundHandler); this.syncTree(newPath); }, copyNode: function(nodeId, parentPath) { this._debug("copyNode"); var originalNode = this.findNode(nodeId); var _this = this; var foundHandler = function(EV, node) { _this.removeEventHandler("syncFound", foundHandler); _this._loadChildNodes(node, null); if (originalNode) _this.selectNode(originalNode, true); $(_this).trigger("nodeCopied", [node]); }; this.addEventHandler("syncFound", foundHandler); this.syncTree(parentPath); }, findNode: function(nodeId, findGlobal) { var _this = this; var branch = this._container.find("li[id='" + nodeId + "']"); if (!findGlobal) branch = branch.filter(function() { return ($(this).attr("umb:type") == _this._activeTreeType); }); var found = branch.length > 0 ? branch : false; this._debug("findNode: " + nodeId + " in '" + this._activeTreeType + "' tree. Found? " + found.length); return found; }, selectNode: function(node, supressEvent, reselect) { + this._debug("selectNode"); var selectedId = this._tree.selected != null ? $(this._tree.selected[0]).attr("id") : null; if (reselect || (selectedId == null || selectedId != node.attr("id"))) { + if (supressEvent) { this._tree.settings.callback.onselect = function() { }; } + this._tree.select_branch(node); var _this = this; this._tree.settings.callback.onselect = function(N, T) { _this.onSelect(N, T) }; + } + }, reloadActionNode: function(supressSelect, supressChildReload, callback) { + this._debug("reloadActionNode: supressSelect = " + supressSelect + ", supressChildReload = " + supressChildReload); if (this._actionNode != null && this._actionNode.jsNode != null) { + var nodeParent = this._actionNode.jsNode.parents("li:first"); this._debug("reloadActionNode: found " + nodeParent.length + " parent nodes"); if (nodeParent.length == 1) { + var nodeDef = this._getNodeDef(nodeParent); this._debug("reloadActionNode: loading ajax for node: " + nodeDef.nodeId); var _this = this; var toReplace = $("
  • " + (this._tree.settings.lang.loading || "Loading ...") + "
  • ").replaceAll(this._actionNode.jsNode); $.get(this._getUrl(nodeDef.sourceUrl), null, function(msg) { + if (!msg || msg.length == 0) { _this._debug("reloadActionNode: error loading ajax data, performing jsTree refresh"); _this._tree.refresh(); if (callback != null) callback.call(_this, false); return; } + var oFound = null; for (var o in msg) { + if (msg[o].attributes != null && msg[o].attributes.id == _this._actionNode.nodeId) { + oFound = _this._tree.parseJSON(msg[o]); if ($(oFound).attr("umb:type") == _this._actionNode.treeType) { break; } + else { oFound = null; } + } + } + if (oFound != null) { + _this._debug("reloadActionNode: node is refreshed!"); var reloaded = $(oFound).replaceAll(toReplace); _this._configureNodes(reloaded, true); if (!supressSelect) _this.selectNode(reloaded); if (!supressChildReload) { _this._loadChildNodes(reloaded, function() { if (callback != null) callback.call(_this, true); }); } + else { if (callback != null) callback.call(_this, true); } + } + else { _this._debug("reloadActionNode: error finding child node in ajax data, performing jsTree refresh"); _this._tree.refresh(); if (callback != null) callback.call(_this, false); } + }, "json"); return; + } + this._debug("reloadActionNode: error finding parent node, performing jsTree refresh"); this._tree.refresh(); if (callback != null) callback.call(this, false); + } + }, getActionNode: function() { return this._actionNode; }, setActiveTreeType: function(treeType) { this._activeTreeType = treeType; }, onNodeDeleting: function(EV) { + this._debug("onNodeDeleting") + this._tree.close_branch(this._actionNode.jsNode); this._actionNode.jsNode.find("a").attr("class", "loading"); this._actionNode.jsNode.find("a").css("background-image", ""); this._actionNode.jsNode.find("a").html(this._uiKeys['deleting']); + }, onNodeDeleted: function(EV) { this._debug("onNodeDeleted"); this._actionNode.jsNode.find("a").removeClass("loading"); this._tree.close_branch(this._actionNode.jsNode); this._actionNode.jsNode.hide("drop", { direction: "down" }, 400); this._updateRecycleBin(); }, onNodeRefresh: function(EV) { this._debug("onNodeRefresh"); this._loadChildNodes(this._actionNode.jsNode, null); }, onSelect: function(NODE, TREE_OBJ) { + this.setActiveTreeType($(NODE).attr("umb:type")); var js = $(NODE).children("a").attr("href").replace("javascript:", ""); this._debug("onSelect: js: " + js); try { var func = eval(js); if (func != null) { func.call(); } } catch (e) { } + return true; + }, onOpen: function(NODE, TREE_OBJ) { this._debug("onOpen: " + $(NODE).attr("id")); var nodes = $(NODE).find("ul > li"); this._configureNodes(nodes); return true; }, onBeforeOpen: function(NODE, TREE_OBJ) { var nodeDef = this._getNodeDef($(NODE)); this._debug("onBeforeOpen: " + nodeDef.nodeId); this._currentAJAXRequest = true; TREE_OBJ.settings.data.url = this._getUrl(nodeDef.sourceUrl); }, onJSONData: function(DATA, TREE_OBJ) { this._debug("onJSONData"); this._currentAJAXRequest = false; return DATA; }, onChange: function(NODE, TREE_OBJ) { + if (this._treeType == "checkbox") { + var $this = $(NODE).is("li") ? $(NODE) : $(NODE).parent(); if ($this.children("a").hasClass("checked")) $this.children("a").removeClass("checked") + else $this.children("a").addClass("checked"); + } + else if (this._treeType == "inheritedcheckbox") { + var $this = $(NODE).is("li") ? $(NODE) : $(NODE).parent(); if ($this.children("a.unchecked").size() == 0) { TREE_OBJ.container.find("a").addClass("unchecked"); } + $this.children("a").removeClass("clicked"); if ($this.children("a").hasClass("checked")) { $this.find("li").andSelf().children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked"); var state = 0; } + else { $this.find("li").andSelf().children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked"); var state = 1; } + $this.parents("li").each(function() { + if (state == 1) { + if ($(this).find("a.unchecked, a.undetermined").size() - 1 > 0) { $(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined"); return false; } + else $(this).children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked"); + } + else { + if ($(this).find("a.checked, a.undetermined").size() - 1 > 0) { $(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined"); return false; } + else $(this).children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked"); + } + }); + } + $(this).trigger("nodeClicked", [NODE]); + }, onRightClick: function(NODE, TREE_OBJ, EV) { this._actionNode = this._getNodeDef($(NODE)); this.setActiveTreeType($(NODE).attr("umb:type")); this._debug("onRightClick: menu = " + this._actionNode.menu); $("div").remove(".tree-default-context"); if (this._actionNode.menu != "") { TREE_OBJ.settings.ui.context = this._getContextMenu(this._actionNode.menu); TREE_OBJ.context_menu(); var timeout = null; TREE_OBJ.context.mouseenter(function() { clearTimeout(timeout); }); TREE_OBJ.context.mouseleave(function() { timeout = setTimeout(function() { TREE_OBJ.hide_context(); }, 400); }); } }, _debug: function(strMsg) { if (this._isDebug) { Sys.Debug.trace("UmbracoTree: " + strMsg); } }, _configureNodes: function(nodes, reconfigure) { + var _this = this; if (!reconfigure) { nodes = nodes.not("li[class*='loaded']"); } + this._debug("_configureNodes: " + nodes.length); var rxInput = new RegExp("\\boverlay-\\w+\\b", "gi"); nodes.each(function() { + if (_this._treeType != "standard") { $(this).children("a:first").css("background", ""); return; } + $(this).children("div").remove(); var m = $(this).attr("class").match(rxInput); if (m != null) { for (i = 0; i < m.length; i++) { _this._debug("_configureNodes: adding overlay: " + m[i] + " for node: " + $(this).attr("id")); $(this).children("a:first").before("
    "); } } + var txt = $(this).children("a").html(); $(this).children("a").html("
    " + txt + "
    "); $(this).addClass("loaded"); + }); + }, _getNodeDef: function(NODE) { var nodedata = $(NODE).children("a").metadata({ type: 'attr', name: 'umb:nodedata' }); this._debug("_getNodeDef: " + $(NODE).attr("id") + ", " + nodedata.nodeType + ", " + nodedata.source); var def = new Umbraco.Controls.NodeDefinition(); def.updateDefinition(this._tree, $(NODE), $(NODE).attr("id"), $(NODE).find("a > div").html(), nodedata.nodeType, nodedata.source, nodedata.menu, $(NODE).attr("umb:type")); return def; }, _updateRecycleBin: function() { this._debug("_updateRecycleBin"); var rNode = this.findNode(this._recycleBinId, true); if (rNode) { this._actionNode = this._getNodeDef(rNode); var _this = this; this.reloadActionNode(true, true, function(success) { if (success) { _this.findNode(_this._recycleBinId, true).effect("highlight", {}, 1000); } }); } }, _loadChildNodes: function(liNode, callback) { this._debug("_loadChildNodes: " + liNode); liNode.removeClass("leaf"); this._tree.close_branch(liNode, true); liNode.children("ul:eq(0)").html(""); this._tree.open_branch(liNode, false, callback); }, _syncTree: function(path, forceReload, numPaths, numAsync) { + this._debug("_syncTree"); var paths = path.split(","); var found = null; var foundIndex = null; if (numPaths == null) numPaths = (paths.length - 0); for (var i = 0; i < numPaths; i++) { foundIndex = paths.length - (1 + i); found = this.findNode(paths[foundIndex]); this._debug("_syncTree: finding... " + paths[foundIndex] + " found? " + found); if (found) break; } + if (!found) { this._debug("no node found in path: " + path + " : " + numPaths); $(this).trigger("syncNotFound", [path]); return; } + if (found.attr("id") != paths[paths.length - 1]) { + var _this = this; this._loadChildNodes(found, function(NODE, TREE_OBJ) { + var pathsToSearch = paths.length - (Number(foundIndex) + 1); if (_this.findNode(paths[foundIndex + 1])) { _this._syncTree(path, forceReload, pathsToSearch, (numAsync == null ? numAsync == 1 : ++numAsync)); } + else { _this._debug("node not found in children: " + path + " : " + numPaths); $(this).trigger("syncNotFound", [path]); } + }); + } + else { + var doReload = (forceReload && (numAsync == null || numAsync < 1)); this._debug("_syncTree: found! numAsync: " + numAsync + ", forceReload: " + forceReload); if (doReload) { this._actionNode = this._getNodeDef(found); this.reloadActionNode(true, true, null); } + else { if (found.attr("id") != "-1") this.selectNode(found, true); this._configureNodes(found, doReload); } + $(this).trigger("syncFound", [found]); + } + }, _getContextMenu: function(strMenu) { + this._debug("_getContextMenu: " + strMenu); var newMenu = new Array(); for (var i = 0; i < strMenu.length; i++) { var letter = strMenu.charAt(i); var menuItem = this._getMenuItemByLetter(letter); if (menuItem != null) newMenu.push(menuItem); } + return newMenu; + }, _getMenuItemByLetter: function(letter) { + if (letter == ",") return "separator"; for (var m in this._fullMenu) { if (this._fullMenu[m].id == letter) { return this._fullMenu[m]; } } + return null; + }, _init: function(jFullMenu, jInitNode, treeContainer, appActions, uiKeys, app, showContext, isDialog, treeType, serviceUrl, dataUrl, umbClientFolder) { + this._debug("_init: creating new tree with class/id: " + treeContainer.attr("class") + " / " + treeContainer.attr("id")); this._fullMenu = jFullMenu; this._initNode = jInitNode; this._menuActions = appActions; this._uiKeys = uiKeys; this._app = app; this._showContext = showContext; this._isDialog = isDialog; this._treeType = treeType; this._serviceUrl = serviceUrl; this._dataUrl = dataUrl; this._umb_clientFolderRoot = umbClientFolder; if (this._menuActions != null) { var _this = this; this._menuActions.addEventHandler("nodeDeleting", function(E) { _this.onNodeDeleting(E) }); this._menuActions.addEventHandler("nodeDeleted", function(E) { _this.onNodeDeleted(E) }); this._menuActions.addEventHandler("nodeRefresh", function(E) { _this.onNodeRefresh(E) }); } + this._container = treeContainer; this._tree = $.tree_create(); this._tree.init(this._container, this._getInitOptions()); this._loadChildNodes(this._container.find("li:first"), null); + }, _getUrl: function(nodeSource) { + if (nodeSource == null || nodeSource == "") { return this._dataUrl; } + var params = nodeSource.split("?")[1]; return this._dataUrl + "?" + params + "&rnd2=" + Umbraco.Utils.generateRandom(); + }, _getInitOptions: function() { this._debug("_getInitOptions"); var _this = this; var options = { data: { type: "json", async: true, url: "", json: this._initNode, async_data: function(NODE) { return null; } }, ui: { dots: false, rtl: false, animation: false, hover_mode: true, theme_path: this._umb_clientFolderRoot + "/Tree/Themes/", theme_name: "umbraco", context: null }, lang: { new_node: "New folder", loading: "
    " + (this._tree.settings.lang.loading || "Loading ...") + "
    " }, rules: { metadata: "umb:nodedata", creatable: "none", draggable: "none" }, callback: { onrgtclk: function(N, T, E) { _this.onRightClick(N, T, E) }, beforeopen: function(N, T) { _this.onBeforeOpen(N, T) }, onopen: function(N, T) { _this.onOpen(N, T) }, onselect: function(N, T) { _this.onSelect(N, T) }, onchange: function(N, T) { _this.onChange(N, T) }, onJSONdata: function(D, T) { return _this.onJSONData(D, T) } } }; return options; } + }; + } +})(jQuery); \ No newline at end of file