From cce9b3172eac0078549d5877d7468c3e80f15e57 Mon Sep 17 00:00:00 2001 From: Lombiq Date: Thu, 28 Jan 2016 21:07:27 +0100 Subject: [PATCH] Implementing Alias Breadcrumb Link to be able to add dynamically generated breadcrumb menu items for content items not being in the menu hierarchy, see #3537 --- .../Drivers/MenuWidgetPartDriver.cs | 2 +- .../Navigation/Services/NavigationManager.cs | 20 ++- .../AliasBreadcrumbLinkMigrations.cs | 22 +++ .../Modules/Orchard.Alias/Migrations.cs | 1 - .../Modules/Orchard.Alias/Module.txt | 5 + .../Navigation/NavigationAliasProvider.cs | 126 ++++++++++++++++++ .../Orchard.Alias/Orchard.Alias.csproj | 2 + .../Navigation/NavigationQueryProvider.cs | 2 +- .../Navigation/TaxonomyNavigationProvider.cs | 2 +- .../UI/Navigation/INavigationFilter.cs | 2 +- .../UI/Navigation/INavigationManager.cs | 2 + 11 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Alias/AliasBreadcrumbLinkMigrations.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Alias/Navigation/NavigationAliasProvider.cs diff --git a/src/Orchard.Web/Core/Navigation/Drivers/MenuWidgetPartDriver.cs b/src/Orchard.Web/Core/Navigation/Drivers/MenuWidgetPartDriver.cs index 2fc5e399a..82c58de8a 100644 --- a/src/Orchard.Web/Core/Navigation/Drivers/MenuWidgetPartDriver.cs +++ b/src/Orchard.Web/Core/Navigation/Drivers/MenuWidgetPartDriver.cs @@ -49,7 +49,7 @@ namespace Orchard.Core.Navigation.Drivers { var menuName = menu.As().Title.HtmlClassify(); var currentCulture = _workContextAccessor.GetContext().CurrentCulture; - var menuItems = _navigationManager.BuildMenu(menu); + var menuItems = _navigationManager.BuildMenu(menu, part.Breadcrumb); var localized = new List(); foreach(var menuItem in menuItems) { // if there is no associated content, it as culture neutral diff --git a/src/Orchard.Web/Core/Navigation/Services/NavigationManager.cs b/src/Orchard.Web/Core/Navigation/Services/NavigationManager.cs index 1caafa12e..044accde8 100644 --- a/src/Orchard.Web/Core/Navigation/Services/NavigationManager.cs +++ b/src/Orchard.Web/Core/Navigation/Services/NavigationManager.cs @@ -44,21 +44,29 @@ namespace Orchard.Core.Navigation.Services { public ILogger Logger { get; set; } public IEnumerable BuildMenu(string menuName) { + return BuildMenu(menuName, false); + } + + public IEnumerable BuildMenu(string menuName, bool isBreadcrumb) { var sources = GetSources(menuName); var hasDebugShowAllMenuItems = _authorizationService.TryCheckAccess(Permission.Named("DebugShowAllMenuItems"), _orchardServices.WorkContext.CurrentUser, null); - return FinishMenu(Reduce(Filter(Merge(sources)), menuName == "admin", hasDebugShowAllMenuItems).ToArray()); + return FinishMenu(Reduce(Filter(Merge(sources), isBreadcrumb), menuName == "admin", hasDebugShowAllMenuItems).ToArray()); } public IEnumerable BuildMenu(IContent menu) { + return BuildMenu(menu,false); + } + + public IEnumerable BuildMenu(IContent menu, bool isBreadcrumb) { var sources = GetSources(menu); var hasDebugShowAllMenuItems = _authorizationService.TryCheckAccess(Permission.Named("DebugShowAllMenuItems"), _orchardServices.WorkContext.CurrentUser, null); - return FinishMenu(Reduce(Arrange(Filter(Merge(sources))), false, hasDebugShowAllMenuItems).ToArray()); + return FinishMenu(Reduce(Arrange(Filter(Merge(sources), isBreadcrumb)), false, hasDebugShowAllMenuItems).ToArray()); } public string GetNextPosition(IContent menu) { var sources = GetSources(menu); var hasDebugShowAllMenuItems = _authorizationService.TryCheckAccess(Permission.Named("DebugShowAllMenuItems"), _orchardServices.WorkContext.CurrentUser, null); - return Position.GetNext(Reduce(Arrange(Filter(Merge(sources))), false, hasDebugShowAllMenuItems).ToArray()); + return Position.GetNext(Reduce(Arrange(Filter(Merge(sources), false)), false, hasDebugShowAllMenuItems).ToArray()); } public IEnumerable BuildImageSets(string menuName) { @@ -74,10 +82,10 @@ namespace Orchard.Core.Navigation.Services { return menuItems; } - private IEnumerable Filter(IEnumerable menuItems) { + private IEnumerable Filter(IEnumerable menuItems, bool isBreadcrumb) { IEnumerable result = menuItems; - foreach(var filter in _navigationFilters) { - result = filter.Filter(result); + foreach (var filter in _navigationFilters) { + result = filter.Filter(result, isBreadcrumb); } return result; diff --git a/src/Orchard.Web/Modules/Orchard.Alias/AliasBreadcrumbLinkMigrations.cs b/src/Orchard.Web/Modules/Orchard.Alias/AliasBreadcrumbLinkMigrations.cs new file mode 100644 index 000000000..4a458ac80 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/AliasBreadcrumbLinkMigrations.cs @@ -0,0 +1,22 @@ +using Orchard.Data.Migration; +using Orchard.ContentManagement.MetaData; +using Orchard.Environment.Extensions; + +namespace Orchard.Alias { + [OrchardFeature("Orchard.Alias.BreadcrumbLink")] + public class AliasBreadcrumbMigration : DataMigrationImpl { + public int Create() { + ContentDefinitionManager.AlterTypeDefinition("AliasBreadcrumbMenuItem", + cfg => cfg + .WithPart("MenuPart") + .WithPart("CommonPart") + .WithIdentity() + .DisplayedAs("Alias Breadcrumb Link") + .WithSetting("Description", "A menu item that can be used to generate breadcrumb links, using Alias data, to content items without explicitly adding links to those content items in the menu.") + .WithSetting("Stereotype", "MenuItem") + ); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs index fc40de006..2e22c2f9e 100644 --- a/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs @@ -27,6 +27,5 @@ namespace Orchard.Alias { ); return 2; } - } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Module.txt b/src/Orchard.Web/Modules/Orchard.Alias/Module.txt index e1eb1deca..4aa262650 100644 --- a/src/Orchard.Web/Modules/Orchard.Alias/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Alias/Module.txt @@ -18,3 +18,8 @@ Features: Description: Synchronizes aliases when created from different servers. Dependencies: Orchard.Alias Category: Content + Orchard.Alias.BreadcrumbLink: + Name: Alias Breadcrumb Link + Description: Provides Alias Breadcrumb Link menu item type that can be used to generate breadcrumb links, using Alias data, to content items without explicitly adding links to those content items in the menu. + Dependencies: Orchard.Alias + Category: Navigation \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Navigation/NavigationAliasProvider.cs b/src/Orchard.Web/Modules/Orchard.Alias/Navigation/NavigationAliasProvider.cs new file mode 100644 index 000000000..ea7ea0cf2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Navigation/NavigationAliasProvider.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.ContentManagement; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Mvc; +using Orchard.UI.Navigation; + +namespace Orchard.Alias.Navigation { + + [OrchardFeature("Orchard.Alias.BreadcrumbLink")] + public class NavigationAliasProvider : INavigationFilter { + private readonly IAliasService _aliasService; + private readonly IContentManager _contentManager; + private readonly IHttpContextAccessor _hca; + + public NavigationAliasProvider( + IAliasService aliasService, + IContentManager contentManager, + IHttpContextAccessor hca) { + _aliasService = aliasService; + _contentManager = contentManager; + _hca = hca; + } + + public IEnumerable Filter(IEnumerable items, bool isBreadcrumb = false) { + foreach (var item in items) { + if (item.Content != null && item.Content.ContentItem.ContentType == "AliasBreadcrumbMenuItem") { + if (isBreadcrumb) { + var request = _hca.Current().Request; + var path = request.Path; + var appPath = request.ApplicationPath ?? "/"; + var requestUrl = (path.StartsWith(appPath) ? path.Substring(appPath.Length) : path).TrimStart('/'); + var endsWithSlash = requestUrl.EndsWith("/"); + + var menuPosition = item.Position; + + var urlLevel = endsWithSlash ? requestUrl.Count(l => l == '/') - 1 : requestUrl.Count(l => l == '/'); + var menuLevel = menuPosition.Count(l => l == '.'); + + // Checking menu item if it's the leaf element or it's an intermediate element + // or it's an unneccessary element according to the url. + RouteValueDictionary contentRoute; + if (menuLevel == urlLevel) { + contentRoute = _aliasService.Get(requestUrl); + } + else { + // If menuLevel doesn't equal with urlLevel it can mean that this menu item is + // an intermediate element (the difference is a positive value) or this menu + // item is lower in the navigation hierarchy according to the url (negative + // value). If the value is negative, removing the menu item, if the value + // is positive finding its place in the hierarchy. + var levelDifference = urlLevel - menuLevel; + if (levelDifference > 0) { + if (endsWithSlash) { + levelDifference += levelDifference; + } + for (int i = 0; i < levelDifference; i++) { + requestUrl = requestUrl.Remove(requestUrl.LastIndexOf('/')); + path = path.Remove(path.LastIndexOf('/')); + } + contentRoute = _aliasService.Get(requestUrl); + if (contentRoute == null) { + // After the exact number of segments is cut out from the url and the + // currentRoute is still null, trying another check with the added slash, + // because we don't know if the alias was created with a slash at the end or not. + contentRoute = _aliasService.Get(requestUrl.Insert(requestUrl.Length, "/")); + path = path.Insert(path.Length, "/"); + } + } + else { + contentRoute = new RouteValueDictionary(); + } + } + + object id; + contentRoute.TryGetValue("Id", out id); + int contentId; + int.TryParse(id as string, out contentId); + if (contentId == 0) { + // If failed to get the Id's value from currentRoute, transform the alias to the virtual path + // and try to get the content item's id from there. E.g. "Blogs/Blog/Item?blogId=12" where + // the last digits represents the content item's id. If there is a match in the path we get + // the digits after the equality sign. + // There is an another type of the routes: like "Contents/Item/Display/13", but when the + // content item's route is in this form we already have the id from contentRoute.TryGetValue("Id", out id). + var virtualPath = _aliasService.LookupVirtualPaths(contentRoute, _hca.Current()).FirstOrDefault(); + int.TryParse(virtualPath != null ? virtualPath.VirtualPath.Substring(virtualPath.VirtualPath.LastIndexOf('=') + 1) : "0", out contentId); + } + if (contentId != 0) { + var currentContentItem = _contentManager.Get(contentId); + if (currentContentItem != null) { + var menuText = _contentManager.GetItemMetadata(currentContentItem).DisplayText; + var routes = _contentManager.GetItemMetadata(currentContentItem).DisplayRouteValues; + + var inserted = new MenuItem { + Text = new LocalizedString(menuText), + IdHint = item.IdHint, + Classes = item.Classes, + Url = path, + Href = item.Href, + LinkToFirstChild = false, + RouteValues = routes, + LocalNav = item.LocalNav, + Items = Enumerable.Empty(), + Position = menuPosition, + Permissions = item.Permissions, + Content = currentContentItem + }; + + yield return inserted; + } + } + } + } + else { + yield return item; + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj b/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj index e1b4b5cda..272305555 100644 --- a/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj +++ b/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj @@ -101,10 +101,12 @@ + + diff --git a/src/Orchard.Web/Modules/Orchard.Projections/Navigation/NavigationQueryProvider.cs b/src/Orchard.Web/Modules/Orchard.Projections/Navigation/NavigationQueryProvider.cs index 988cf8c49..3dd6a366c 100644 --- a/src/Orchard.Web/Modules/Orchard.Projections/Navigation/NavigationQueryProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Projections/Navigation/NavigationQueryProvider.cs @@ -21,7 +21,7 @@ namespace Orchard.Projections.Navigation { _projectionManager = projectionManager; } - public IEnumerable Filter(IEnumerable items) { + public IEnumerable Filter(IEnumerable items, bool isBreadcrumb) { foreach (var item in items) { if (item.Content != null && item.Content.ContentItem.ContentType == "NavigationQueryMenuItem") { diff --git a/src/Orchard.Web/Modules/Orchard.Taxonomies/Navigation/TaxonomyNavigationProvider.cs b/src/Orchard.Web/Modules/Orchard.Taxonomies/Navigation/TaxonomyNavigationProvider.cs index 6ca41931c..52ee07004 100644 --- a/src/Orchard.Web/Modules/Orchard.Taxonomies/Navigation/TaxonomyNavigationProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Taxonomies/Navigation/TaxonomyNavigationProvider.cs @@ -24,7 +24,7 @@ namespace Orchard.Taxonomies.Navigation { _taxonomyService = taxonomyService; } - public IEnumerable Filter(IEnumerable items) { + public IEnumerable Filter(IEnumerable items, bool isBreadcrumb) { foreach (var item in items) { if (item.Content != null && item.Content.ContentItem.ContentType == "TaxonomyNavigationMenuItem") { diff --git a/src/Orchard/UI/Navigation/INavigationFilter.cs b/src/Orchard/UI/Navigation/INavigationFilter.cs index cd51420bd..679dee49b 100644 --- a/src/Orchard/UI/Navigation/INavigationFilter.cs +++ b/src/Orchard/UI/Navigation/INavigationFilter.cs @@ -5,6 +5,6 @@ namespace Orchard.UI.Navigation { /// Provides a way to alter the main navigation, for instance by dynamically injecting new items /// public interface INavigationFilter : IDependency { - IEnumerable Filter(IEnumerable menuItems); + IEnumerable Filter(IEnumerable menuItems, bool isBreadcrumb = false); } } diff --git a/src/Orchard/UI/Navigation/INavigationManager.cs b/src/Orchard/UI/Navigation/INavigationManager.cs index 0c0045f1d..6ea24dfe3 100644 --- a/src/Orchard/UI/Navigation/INavigationManager.cs +++ b/src/Orchard/UI/Navigation/INavigationManager.cs @@ -5,7 +5,9 @@ using Orchard.ContentManagement; namespace Orchard.UI.Navigation { public interface INavigationManager : IDependency { IEnumerable BuildMenu(string menuName); + IEnumerable BuildMenu(string menuName, bool isBreadcrumbMenu); IEnumerable BuildMenu(IContent menu); + IEnumerable BuildMenu(IContent menu, bool isBreadcrumbMenu); IEnumerable BuildImageSets(string menuName); string GetUrl(string menuItemUrl, RouteValueDictionary routeValueDictionary); string GetNextPosition(IContent menu);