diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 5f2a8536..d2edd289 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -384,21 +384,24 @@ private void ThemeSettings() { _themeSettingsType = null; - var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); - if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) - { - _themeSettingsType = Type.GetType(theme.ThemeSettingsType); - if (_themeSettingsType != null) - { - ThemeSettingsComponent = builder => - { - builder.OpenComponent(0, _themeSettingsType); - builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - builder.CloseComponent(); - }; - } - _refresh = true; - } + if (PageState.QueryString.ContainsKey("cp")) // can only be displayed if invoked from Control Panel + { + var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); + if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) + { + _themeSettingsType = Type.GetType(theme.ThemeSettingsType); + if (_themeSettingsType != null) + { + ThemeSettingsComponent = builder => + { + builder.OpenComponent(0, _themeSettingsType); + builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); + builder.CloseComponent(); + }; + } + _refresh = true; + } + } } private async Task SavePage() diff --git a/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor b/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor index 24a5da86..a2d9ba7c 100644 --- a/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor +++ b/Oqtane.Client/Themes/Controls/Container/ModuleTitle.razor @@ -3,7 +3,9 @@ @attribute [OqtaneIgnore] - @((MarkupString)title) + + @((MarkupString)title) + @code { diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 28b72307..43325047 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -279,5 +279,19 @@ namespace Oqtane.UI } } + public Task ScrollToId(string id) + { + try + { + _jsRuntime.InvokeVoidAsync( + "Oqtane.Interop.scrollToId", + id); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 360ff6a1..ad520fb5 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -11,6 +11,7 @@ @inject IModuleService ModuleService @inject IUrlMappingService UrlMappingService @inject ILogService LogService +@inject IJSRuntime JSRuntime @implements IHandleAfterRender @DynamicComponent @@ -234,6 +235,7 @@ }; OnStateChange?.Invoke(_pagestate); + await ScrollToFragment(_pagestate.Uri); } } else // page not found @@ -242,7 +244,7 @@ var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath); if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) { - var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; + var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; NavigationManager.NavigateTo(url, false); } else // not mapped @@ -262,238 +264,259 @@ } } } - } + } + } + else + { + // site does not exist + } + } + + private async void LocationChanged(object sender, LocationChangedEventArgs args) + { + _absoluteUri = args.Location; + await Refresh(); + } + + Task IHandleAfterRender.OnAfterRenderAsync() + { + if (!_navigationInterceptionEnabled) + { + _navigationInterceptionEnabled = true; + return NavigationInterception.EnableNavigationInterceptionAsync(); + } + return Task.CompletedTask; + } + + private Dictionary ParseQueryString(string query) + { + Dictionary querystring = new Dictionary(StringComparer.OrdinalIgnoreCase); // case insensistive keys + if (!string.IsNullOrEmpty(query)) + { + if (query.StartsWith("?")) + { + query = query.Substring(1); // ignore "?" + } + foreach (string kvp in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (kvp != "") + { + if (kvp.Contains("=")) + { + string[] pair = kvp.Split('='); + querystring.Add(pair[0], pair[1]); + } + else + { + querystring.Add(kvp, "true"); // default parameter when no value is provided + } + } + } + } + return querystring; + } + + private async Task ProcessPage(Page page, Site site, User user) + { + try + { + if (page.IsPersonalizable && user != null) + { + // load the personalized page + page = await PageService.GetPageAsync(page.PageId, user.UserId); + } + + if (string.IsNullOrEmpty(page.ThemeType)) + { + page.ThemeType = site.DefaultThemeType; + } + + page.Panes = new List(); + page.Resources = new List(); + + string panes = PaneNames.Admin; + Type themetype = Type.GetType(page.ThemeType); + if (themetype == null) + { + // fallback + page.ThemeType = Constants.DefaultTheme; + themetype = Type.GetType(Constants.DefaultTheme); + } + if (themetype != null) + { + var themeobject = Activator.CreateInstance(themetype) as IThemeControl; + if (themeobject != null) + { + if (!string.IsNullOrEmpty(themeobject.Panes)) + { + panes = themeobject.Panes; + } + page.Resources = ManagePageResources(page.Resources, themeobject.Resources); + } + } + page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + catch + { + // error loading theme or layout + } + + return page; + } + + private (Page Page, List Modules) ProcessModules(Page page, List modules, int moduleid, string action, string defaultcontainertype) + { + var paneindex = new Dictionary(); + foreach (Module module in modules) + { + // initialize module control properties + module.SecurityAccessLevel = SecurityAccessLevel.Host; + module.ControlTitle = ""; + module.Actions = ""; + module.UseAdminContainer = false; + module.PaneModuleIndex = -1; + module.PaneModuleCount = 0; + + if ((module.PageId == page.PageId || module.ModuleId == moduleid)) + { + var typename = Constants.ErrorModule; + if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) + { + typename = module.ModuleDefinition.ControlTypeTemplate; + + // handle default action + if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) + { + action = module.ModuleDefinition.DefaultAction; + } + + // check if the module defines custom action routes + if (module.ModuleDefinition.ControlTypeRoutes != "") + { + foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (route.StartsWith(action + "=")) + { + typename = route.Replace(action + "=", ""); + } + } + } + } + + // ensure component exists and implements IModuleControl + module.ModuleType = ""; + if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase)) + { + typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action); + } + else + { + typename = typename.Replace(Constants.ActionToken, action); + } + Type moduletype = Type.GetType(typename, false, true); // case insensitive + if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) + { + module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name + } + + // get additional metadata from IModuleControl interface + if (moduletype != null && module.ModuleType != "") + { + // retrieve module component resources + var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); + if (action.ToLower() == "settings" && module.ModuleDefinition != null) + { + // settings components are embedded within a framework settings module + moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); + if (moduletype != null) + { + moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); + } + } + + // additional metadata needed for admin components + if (module.ModuleId == moduleid && action != "") + { + module.SecurityAccessLevel = moduleobject.SecurityAccessLevel; + module.ControlTitle = moduleobject.Title; + module.Actions = moduleobject.Actions; + module.UseAdminContainer = moduleobject.UseAdminContainer; + } + } + + // ensure module's pane exists in current page and if not, assign it to the Admin pane + if (page.Panes == null || page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1) + { + module.Pane = PaneNames.Admin; + } + + // calculate module position within pane + if (paneindex.ContainsKey(module.Pane.ToLower())) + { + paneindex[module.Pane.ToLower()] += 1; + } + else + { + paneindex.Add(module.Pane.ToLower(), 0); + } + + module.PaneModuleIndex = paneindex[module.Pane.ToLower()]; + + // container fallback + if (string.IsNullOrEmpty(module.ContainerType)) + { + module.ContainerType = defaultcontainertype; + } + } + } + + foreach (Module module in modules.Where(item => item.PageId == page.PageId)) + { + if (paneindex.ContainsKey(module.Pane.ToLower())) + { + module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1; + } + } + + return (page, modules); + } + + private List ManagePageResources(List pageresources, List resources) + { + if (resources != null) + { + foreach (var resource in resources) + { + // ensure resource does not exist already + if (pageresources.Find(item => item.Url == resource.Url) == null) + { + pageresources.Add(resource); + } + } + } + return pageresources; + } + + private async Task ScrollToFragment(Uri uri) + { + var fragment = uri.Fragment; + if (fragment.StartsWith('#')) + { + // handle text fragment (https://example.org/#test:~:text=foo) + var id = fragment.Substring(1); + var index = id.IndexOf(":~:", StringComparison.Ordinal); + if (index > 0) + { + id = id.Substring(0, index); + } + + if (!string.IsNullOrEmpty(id)) + { + var interop = new Interop(JSRuntime); + await interop.ScrollToId(id); + } } - else - { - // site does not exist - } - } - - private async void LocationChanged(object sender, LocationChangedEventArgs args) - { - _absoluteUri = args.Location; - await Refresh(); - } - - Task IHandleAfterRender.OnAfterRenderAsync() - { - if (!_navigationInterceptionEnabled) - { - _navigationInterceptionEnabled = true; - return NavigationInterception.EnableNavigationInterceptionAsync(); - } - return Task.CompletedTask; - } - - private Dictionary ParseQueryString(string query) - { - Dictionary querystring = new Dictionary(StringComparer.OrdinalIgnoreCase); // case insensistive keys - if (!string.IsNullOrEmpty(query)) - { - if (query.StartsWith("?")) - { - query = query.Substring(1); // ignore "?" - } - foreach (string kvp in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (kvp != "") - { - if (kvp.Contains("=")) - { - string[] pair = kvp.Split('='); - querystring.Add(pair[0], pair[1]); - } - else - { - querystring.Add(kvp, "true"); // default parameter when no value is provided - } - } - } - } - return querystring; - } - - private async Task ProcessPage(Page page, Site site, User user) - { - try - { - if (page.IsPersonalizable && user != null) - { - // load the personalized page - page = await PageService.GetPageAsync(page.PageId, user.UserId); - } - - if (string.IsNullOrEmpty(page.ThemeType)) - { - page.ThemeType = site.DefaultThemeType; - } - - page.Panes = new List(); - page.Resources = new List(); - - string panes = PaneNames.Admin; - Type themetype = Type.GetType(page.ThemeType); - if (themetype == null) - { - // fallback - page.ThemeType = Constants.DefaultTheme; - themetype = Type.GetType(Constants.DefaultTheme); - } - if (themetype != null) - { - var themeobject = Activator.CreateInstance(themetype) as IThemeControl; - if (themeobject != null) - { - if (!string.IsNullOrEmpty(themeobject.Panes)) - { - panes = themeobject.Panes; - } - page.Resources = ManagePageResources(page.Resources, themeobject.Resources); - } - } - page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - catch - { - // error loading theme or layout - } - - return page; - } - - private (Page Page, List Modules) ProcessModules(Page page, List modules, int moduleid, string action, string defaultcontainertype) - { - var paneindex = new Dictionary(); - foreach (Module module in modules) - { - // initialize module control properties - module.SecurityAccessLevel = SecurityAccessLevel.Host; - module.ControlTitle = ""; - module.Actions = ""; - module.UseAdminContainer = false; - module.PaneModuleIndex = -1; - module.PaneModuleCount = 0; - - if ((module.PageId == page.PageId || module.ModuleId == moduleid)) - { - var typename = Constants.ErrorModule; - if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) - { - typename = module.ModuleDefinition.ControlTypeTemplate; - - // handle default action - if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) - { - action = module.ModuleDefinition.DefaultAction; - } - - // check if the module defines custom action routes - if (module.ModuleDefinition.ControlTypeRoutes != "") - { - foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (route.StartsWith(action + "=")) - { - typename = route.Replace(action + "=", ""); - } - } - } - } - - // ensure component exists and implements IModuleControl - module.ModuleType = ""; - if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase)) - { - typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action); - } - else - { - typename = typename.Replace(Constants.ActionToken, action); - } - Type moduletype = Type.GetType(typename, false, true); // case insensitive - if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) - { - module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name - } - - // get additional metadata from IModuleControl interface - if (moduletype != null && module.ModuleType != "") - { - // retrieve module component resources - var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); - if (action.ToLower() == "settings" && module.ModuleDefinition != null) - { - // settings components are embedded within a framework settings module - moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); - if (moduletype != null) - { - moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources); - } - } - - // additional metadata needed for admin components - if (module.ModuleId == moduleid && action != "") - { - module.SecurityAccessLevel = moduleobject.SecurityAccessLevel; - module.ControlTitle = moduleobject.Title; - module.Actions = moduleobject.Actions; - module.UseAdminContainer = moduleobject.UseAdminContainer; - } - } - - // ensure module's pane exists in current page and if not, assign it to the Admin pane - if (page.Panes == null || page.Panes.FindIndex(item => item.Equals(module.Pane, StringComparison.OrdinalIgnoreCase)) == -1) - { - module.Pane = PaneNames.Admin; - } - - // calculate module position within pane - if (paneindex.ContainsKey(module.Pane.ToLower())) - { - paneindex[module.Pane.ToLower()] += 1; - } - else - { - paneindex.Add(module.Pane.ToLower(), 0); - } - - module.PaneModuleIndex = paneindex[module.Pane.ToLower()]; - - // container fallback - if (string.IsNullOrEmpty(module.ContainerType)) - { - module.ContainerType = defaultcontainertype; - } - } - } - - foreach (Module module in modules.Where(item => item.PageId == page.PageId)) - { - if (paneindex.ContainsKey(module.Pane.ToLower())) - { - module.PaneModuleCount = paneindex[module.Pane.ToLower()] + 1; - } - } - - return (page, modules); - } - - private List ManagePageResources(List pageresources, List resources) - { - if (resources != null) - { - foreach (var resource in resources) - { - // ensure resource does not exist already - if (pageresources.Find(item => item.Url == resource.Url) == null) - { - pageresources.Add(resource); - } - } - } - return pageresources; } } diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 9d562067..0185a5c2 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -3,7 +3,7 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @model Oqtane.Pages.HostModel - + diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 1d9004de..1a882402 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -52,6 +52,7 @@ namespace Oqtane.Pages _settings = settings; } + public string Language = "en"; public string AntiForgeryToken = ""; public string Runtime = "Server"; public RenderMode RenderMode = RenderMode.Server; @@ -174,19 +175,29 @@ namespace Oqtane.Pages } // set culture if not specified - if (HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] == null) + string culture = HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; + if (culture == null) { - // set default language for site if the culture is not supported + // get default language for site var languages = _languages.GetLanguages(alias.SiteId); - if (languages.Any() && languages.All(l => l.Code != CultureInfo.CurrentUICulture.Name)) + if (languages.Any()) { - var defaultLanguage = languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First(); - SetLocalizationCookie(defaultLanguage.Code); + // use default language if specified otherwise use first language in collection + culture = (languages.Where(l => l.IsDefault).SingleOrDefault() ?? languages.First()).Code; } else { - SetLocalizationCookie(_localizationManager.GetDefaultCulture()); + culture = _localizationManager.GetDefaultCulture(); } + SetLocalizationCookie(culture); + } + + // set language for page + if (!string.IsNullOrEmpty(culture)) + { + // localization cookie value in form of c=en|uic=en + Language = culture.Split('|')[0]; + Language = Language.Replace("c=", ""); } } } diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 1b3b4e77..7f72eb9c 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -125,15 +125,18 @@ namespace Oqtane.Repository foreach (Type containertype in containertypes) { var containerobject = Activator.CreateInstance(containertype) as IThemeControl; - theme.Containers.Add( - new ThemeControl - { - TypeName = containertype.FullName + ", " + themeControlType.Assembly.GetName().Name, - Name = (string.IsNullOrEmpty(containerobject.Name)) ? Utilities.GetTypeNameLastSegment(containertype.FullName, 0) : containerobject.Name, - Thumbnail = containerobject.Thumbnail, - Panes = "" - } - ); + if (theme.Containers.FirstOrDefault(item => item.TypeName == containertype.FullName + ", " + themeControlType.Assembly.GetName().Name) == null) + { + theme.Containers.Add( + new ThemeControl + { + TypeName = containertype.FullName + ", " + themeControlType.Assembly.GetName().Name, + Name = (string.IsNullOrEmpty(containerobject.Name)) ? Utilities.GetTypeNameLastSegment(containertype.FullName, 0) : containerobject.Name, + Thumbnail = containerobject.Thumbnail, + Panes = "" + } + ); + } } themes[index] = theme; diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index a3559989..5da25ceb 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -126,6 +126,10 @@ app { margin-bottom: 15px; } +.app-moduletitle a { + scroll-margin-top: 7rem; +} + /* Tooltips */ .app-tooltip { cursor: help; diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index e4e07e8e..dcecde3c 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -376,5 +376,14 @@ Oqtane.Interop = { left: left, behavior: behavior }); + }, + scrollToId: function (id) { + var element = document.getElementById(id); + if (element instanceof HTMLElement) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); } -}; +}}; diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 0085b45f..aa70e703 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -62,7 +62,7 @@ namespace Oqtane.Shared { [Obsolete(RoleObsoleteMessage)] public const string RegisteredRole = RoleNames.Registered; - public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico"; + public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp"; public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$";