diff --git a/Oqtane.Client/Themes/BlazorTheme/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Default.razor index ecd3596e..4fa2a01d 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Default.razor @@ -28,9 +28,19 @@ @code { public override string Panes => "Content"; + public override List Resources + { + get + { + List resources = new List(); + resources.Add(new Resource { ResourceType = ResourceType.Stylesheet, Url = "Themes/" + GetType().Namespace + "/Theme.css", Integrity = "", CrossOrigin = "" }); + return resources; + } + } + protected override async Task OnParametersSetAsync() { - await IncludeCSS("Theme.css"); + //await IncludeCSS("Theme.css"); } } \ No newline at end of file diff --git a/Oqtane.Client/Themes/OqtaneTheme/Default.razor b/Oqtane.Client/Themes/OqtaneTheme/Default.razor index f92823e7..f6238a17 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/Default.razor +++ b/Oqtane.Client/Themes/OqtaneTheme/Default.razor @@ -19,11 +19,22 @@ @code { public override string Panes => string.Empty; + public override List Resources + { + get + { + List resources = new List(); + resources.Add(new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css", Integrity = "sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM", CrossOrigin = "anonymous" }); + resources.Add(new Resource { ResourceType = ResourceType.Stylesheet, Url = "Themes/" + GetType().Namespace + "/Theme.css" }); + return resources; + } + } + protected override async Task OnParametersSetAsync() { - // go to https://www.bootstrapcdn.com/bootswatch/ and take your favorite theme - // - await LoadBootstrapTheme("https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css","sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM"); - await IncludeCSS("Theme.css"); + // go to https://www.bootstrapcdn.com/bootswatch/ and take your favorite theme + // + //await LoadBootstrapTheme("https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/cyborg/bootstrap.min.css","sha384-l7xaoY0cJM4h9xh1RfazbgJVUZvdtyLWPueWNtLAphf/UbBgOVzqbOTogxPwYLHM"); + //await IncludeCSS("Theme.css"); } } diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 83694766..3e3136f5 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using Oqtane.Models; using Oqtane.Shared; using Oqtane.UI; +using System.Collections.Generic; using System.Threading.Tasks; namespace Oqtane.Themes @@ -14,6 +16,7 @@ namespace Oqtane.Themes [CascadingParameter] protected PageState PageState { get; set; } public virtual string Panes { get; set; } + public virtual List Resources { get; set; } public string ThemePath() { diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 73aaf029..0ae86ac5 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -118,6 +118,22 @@ namespace Oqtane.UI } } + public Task RemoveElementsById(string prefix, string first, string last) + { + try + { + _jsRuntime.InvokeAsync( + "interop.removeElementsById", + prefix, first, last); + return Task.CompletedTask; + } + catch + { + return Task.CompletedTask; + } + } + + public ValueTask GetElementByName(string name) { try diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 488c43bf..37676dff 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -355,19 +355,30 @@ page.ThemeType = site.DefaultThemeType; page.LayoutType = site.DefaultLayoutType; } - Type type; + + page.Resources = new List(); + + Type themetype = Type.GetType(page.ThemeType); + var themeobject = Activator.CreateInstance(themetype); + if (themeobject != null) + { + page.Panes = (string)themetype.GetProperty("Panes").GetValue(themeobject, null); + var resources = (List)themetype.GetProperty("Resources").GetValue(themeobject, null); + if (resources != null) + { + page.Resources.AddRange(resources); + } + } if (!string.IsNullOrEmpty(page.LayoutType)) { - type = Type.GetType(page.LayoutType); + themetype = Type.GetType(page.LayoutType); + themeobject = Activator.CreateInstance(themetype); + if (themeobject != null) + { + page.Panes = (string)themetype.GetProperty("Panes").GetValue(themeobject, null); + } } - else - { - type = Type.GetType(page.ThemeType); - } - - var property = type.GetProperty("Panes"); - page.Panes = (string)property.GetValue(Activator.CreateInstance(type), null); } catch { diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index e2cf9e4a..9289fbfd 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -12,6 +12,8 @@ protected override async Task OnParametersSetAsync() { var interop = new Interop(JsRuntime); + + // set page title if (!string.IsNullOrEmpty(PageState.Page.Title)) { await interop.UpdateTitle(PageState.Page.Title); @@ -20,10 +22,30 @@ { await interop.UpdateTitle(PageState.Site.Name + " - " + PageState.Page.Name); } + + // manage page resources- they cannot be removed first and then added because the browser will "flash" and result in a poor user experience - they need to be updated + int index = 0; + foreach (Resource resource in PageState.Page.Resources) + { + index += 1; + switch (resource.ResourceType) + { + case ResourceType.Stylesheet: + await interop.IncludeLink("app-resource" + index.ToString("00"), "stylesheet", resource.Url, "text/css", resource.Integrity, resource.CrossOrigin); + break; + case ResourceType.Script: + break; + } + } + // remove any page resources references which are no longer required for this page + await interop.RemoveElementsById("app-resource", "app-resource" + (index + 1).ToString("00"), ""); + + // add favicon if (PageState.Site.FaviconFileId != null) { await interop.IncludeLink("fav-icon", "shortcut icon", Utilities.ContentUrl(PageState.Alias, PageState.Site.FaviconFileId.Value), "image/x-icon", "", ""); } + // add PWA support if (PageState.Site.PwaIsEnabled) { await InitializePwa(interop); diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 5f41709c..36b6c4f3 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -160,6 +160,15 @@ window.interop = { } } }, + removeElementsById: function (prefix, first, last) { + var elements = document.querySelectorAll('[id^=' + prefix + ']'); + for (var i = elements.length - 1; i >= 0; i--) { + var element = elements[i]; + if (element.id.startsWith(prefix) && (first === '' || element.id >= first) && (last === '' || element.id <= last)) { + element.parentNode.removeChild(element); + } + } + }, getElementByName: function (name) { var elements = document.getElementsByName(name); if (elements.length) { diff --git a/Oqtane.Shared/Enums/ResourceType.cs b/Oqtane.Shared/Enums/ResourceType.cs new file mode 100644 index 00000000..b2196652 --- /dev/null +++ b/Oqtane.Shared/Enums/ResourceType.cs @@ -0,0 +1,8 @@ +namespace Oqtane.Shared +{ + public enum ResourceType + { + Stylesheet, + Script + } +} diff --git a/Oqtane.Shared/Interfaces/IThemeControl.cs b/Oqtane.Shared/Interfaces/IThemeControl.cs index e9e19e7c..0f6ad00a 100644 --- a/Oqtane.Shared/Interfaces/IThemeControl.cs +++ b/Oqtane.Shared/Interfaces/IThemeControl.cs @@ -1,7 +1,11 @@ -namespace Oqtane.Themes +using Oqtane.Models; +using System.Collections.Generic; + +namespace Oqtane.Themes { public interface IThemeControl { string Panes { get; } // identifies all panes in a theme ( delimited by ";" ) - assumed to be a layout if no panes specified + List Resources { get; } // identifies all resources in a theme } } diff --git a/Oqtane.Shared/Models/Page.cs b/Oqtane.Shared/Models/Page.cs index 2d3f3624..6512580f 100644 --- a/Oqtane.Shared/Models/Page.cs +++ b/Oqtane.Shared/Models/Page.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; namespace Oqtane.Models @@ -33,6 +34,8 @@ namespace Oqtane.Models [NotMapped] public string Panes { get; set; } [NotMapped] + public List Resources { get; set; } + [NotMapped] public string Permissions { get; set; } [NotMapped] public int Level { get; set; } diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs new file mode 100644 index 00000000..81dc50fd --- /dev/null +++ b/Oqtane.Shared/Models/Resource.cs @@ -0,0 +1,12 @@ +using Oqtane.Shared; + +namespace Oqtane.Models +{ + public class Resource + { + public ResourceType ResourceType { get; set; } + public string Url { get; set; } + public string Integrity { get; set; } + public string CrossOrigin { get; set; } + } +} diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index a2dd83df..d5a29552 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -1,4 +1,6 @@ -namespace Oqtane.Models +using System.Collections.Generic; + +namespace Oqtane.Models { public class Theme {