├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── pkg ├── icon.png └── pack.ps1 ├── src ├── .editorconfig ├── Directory.Build.props ├── Perplex.ContentBlocks.Core │ ├── Categories │ │ ├── Constants.Categories.cs │ │ ├── ContentBlockCategory.cs │ │ ├── ContentBlockCategoryComposer.cs │ │ ├── IContentBlockCategory.cs │ │ ├── IContentBlockCategoryRepository.cs │ │ └── InMemoryContentBlockCategoryRepository.cs │ ├── Definitions │ │ ├── ContentBlockDefinition.cs │ │ ├── ContentBlockDefinitionComposer.cs │ │ ├── ContentBlockDefinitionFilterer.cs │ │ ├── ContentBlockDefinition{T}.cs │ │ ├── ContentBlockLayout.cs │ │ ├── ContentBlocksDefinitionApiController.cs │ │ ├── IContentBlockDefinition.cs │ │ ├── IContentBlockDefinitionFilterer.cs │ │ ├── IContentBlockDefinitionRepository.cs │ │ ├── IContentBlockDefinition{T}.cs │ │ ├── IContentBlockLayout.cs │ │ └── InMemoryContentBlockDefinitionRepository.cs │ ├── Perplex.ContentBlocks.Core.csproj │ ├── Presets │ │ ├── ContentBlockPreset.cs │ │ ├── ContentBlockPresetsComposer.cs │ │ ├── ContentBlockVariantPreset.cs │ │ ├── ContentBlocksPreset.cs │ │ ├── ContentBlocksPresetApiController.cs │ │ ├── IContentBlockPreset.cs │ │ ├── IContentBlockVariantPreset.cs │ │ ├── IContentBlocksPreset.cs │ │ ├── IContentBlocksPresetRepository.cs │ │ └── InMemoryContentBlocksPresetRepository.cs │ ├── Preview │ │ ├── Constants.Preview.cs │ │ ├── ContentBlocksPreviewApiController.cs │ │ ├── ContentBlocksPreviewComposer.cs │ │ ├── CookieBasedPreviewModeProvider.cs │ │ ├── DefaultPreviewScrollScriptProvider.cs │ │ ├── IPreviewModeProvider.cs │ │ └── IPreviewScrollScriptProvider.cs │ ├── PropertyEditor │ │ ├── Configuration │ │ │ ├── Constants.PropertyEditor.Configuration.cs │ │ │ ├── ContentBlocksConfiguration.cs │ │ │ └── ContentBlocksConfigurationEditor.cs │ │ ├── Constants.PropertyEditor.cs │ │ ├── ContentBlocksPropertyEditor.cs │ │ ├── ContentBlocksValidator.cs │ │ ├── ContentBlocksValueConverter.cs │ │ ├── ContentBlocksValueEditor.cs │ │ └── ModelValue │ │ │ ├── ContentBlockInterValue.cs │ │ │ ├── ContentBlockModelValue.cs │ │ │ ├── ContentBlockVariantModelValue.cs │ │ │ ├── ContentBlocksInterValue.cs │ │ │ ├── ContentBlocksModelValue.cs │ │ │ ├── ContentBlocksModelValueComposer.cs │ │ │ ├── ContentBlocksModelValueCopyingHandler.cs │ │ │ └── ContentBlocksModelValueDeserializer.cs │ ├── Providers │ │ ├── ContentBlockProvidersComposer.cs │ │ ├── DocumentTypeAliasProvider.cs │ │ └── IDocumentTypeAliasProvider.cs │ ├── Rendering │ │ ├── ContentBlockRenderer.cs │ │ ├── ContentBlockViewModel.cs │ │ ├── ContentBlockViewModelFactory{T}.cs │ │ ├── ContentBlockViewModel{T}.cs │ │ ├── ContentBlocks.cs │ │ ├── ContentBlocksRenderer.cs │ │ ├── ContentBlocksRenderingComposer.cs │ │ ├── ContentBlocksTagHelper.cs │ │ ├── Delegates.cs │ │ ├── HtmlHelperExtensions.Rendering.cs │ │ ├── IContentBlockRenderer.cs │ │ ├── IContentBlockViewModel.cs │ │ ├── IContentBlockViewModelFactory.cs │ │ ├── IContentBlockViewModelFactory{T}.cs │ │ ├── IContentBlockViewModel{T}.cs │ │ ├── IContentBlocks.cs │ │ └── IContentBlocksRenderer.cs │ ├── Utils │ │ ├── ContentBlockUtils.cs │ │ ├── ContentBlockUtilsComposer.cs │ │ └── Cookies │ │ │ ├── CookiesComposer.cs │ │ │ ├── HttpCookiesAccessor.cs │ │ │ └── IHttpCookiesAccessor.cs │ └── Variants │ │ ├── ContentBlockDefaultVariantSelector.cs │ │ ├── ContentBlocksVariantsComposer.cs │ │ └── IContentBlockVariantSelector.cs ├── Perplex.ContentBlocks.DeliveryApi │ ├── ApiContentBlockViewModel.cs │ ├── ApiContentBlocks.cs │ ├── Composer.cs │ ├── ContentBlocksApiValueConverter.cs │ ├── IApiContentBlockViewModel.cs │ ├── IApiContentBlocks.cs │ └── Perplex.ContentBlocks.DeliveryApi.csproj ├── Perplex.ContentBlocks.StaticAssets │ ├── .gitignore │ ├── ManifestFilter.cs │ ├── ManifestFilterComposer.cs │ ├── Perplex.ContentBlocks.StaticAssets.csproj │ ├── build │ │ └── Perplex.ContentBlocks.StaticAssets.targets │ └── src │ │ ├── App_Plugins │ │ └── Perplex.ContentBlocks │ │ │ ├── assets │ │ │ └── icons.svg │ │ │ ├── components │ │ │ ├── perplex.content-block.component.html │ │ │ ├── perplex.content-block.component.js │ │ │ ├── perplex.content-blocks.add-block.html │ │ │ ├── perplex.content-blocks.add-block.js │ │ │ ├── perplex.content-blocks.custom-component.js │ │ │ ├── perplex.content-blocks.custom-components.js │ │ │ ├── perplex.content-blocks.icon.js │ │ │ └── perplex.content-blocks.nested-content-patch.js │ │ │ ├── configuration │ │ │ ├── perplex.content-blocks.configuration.structure-view.html │ │ │ └── perplex.content-blocks.configuration.structure.js │ │ │ ├── lang │ │ │ ├── da-DK.xml │ │ │ ├── en-US.xml │ │ │ └── nl-NL.xml │ │ │ ├── lib │ │ │ ├── angular-slick.js │ │ │ └── slick.min.js │ │ │ ├── perplex.content-blocks.api.js │ │ │ ├── perplex.content-blocks.controller.js │ │ │ ├── perplex.content-blocks.html │ │ │ ├── perplex.content-blocks.less │ │ │ ├── perplex.content-blocks.requires.js │ │ │ ├── styles │ │ │ ├── perplex.content-blocks-block.less │ │ │ ├── perplex.content-blocks-editor.less │ │ │ ├── perplex.content-blocks-grid.less │ │ │ ├── perplex.content-blocks-icon.less │ │ │ ├── perplex.content-blocks-icons.less │ │ │ ├── perplex.content-blocks-overlay.less │ │ │ ├── perplex.content-blocks-picker.less │ │ │ ├── perplex.content-blocks-sidebar.less │ │ │ └── perplex.content-blocks-slicksliders.less │ │ │ └── utils │ │ │ ├── perplex.content-blocks.copy-paste-service.js │ │ │ ├── perplex.content-blocks.utils.js │ │ │ ├── portal │ │ │ └── perplex.content-blocks-portal.js │ │ │ ├── property │ │ │ ├── perplex.content-blocks.property-scaffold-cache.js │ │ │ ├── perplex.content-blocks.property.html │ │ │ └── perplex.content-blocks.property.js │ │ │ └── tab-focus │ │ │ ├── perplex.content-blocks.tab-focus-once.directive.js │ │ │ └── perplex.content-blocks.tab-focus.service.js │ │ ├── gulpfile.js │ │ ├── package.json │ │ └── pnpm-lock.yaml ├── Perplex.ContentBlocks.sln └── Perplex.ContentBlocks │ └── Perplex.ContentBlocks.csproj └── version /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | .config/ 4 | .github/ 5 | bin/ 6 | obj/ 7 | node_modules/ 8 | 9 | **/*.nupkg 10 | **/App_Plugins/**/*.css 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "printWidth": 120, 5 | "semi": true, 6 | "singleQuote": false, 7 | "useTabs": false, 8 | "arrowParens": "avoid", 9 | "overrides": [ 10 | { 11 | "files": "*.md", 12 | "options": { 13 | "tabWidth": 2 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Perplex Digital 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerplexDigital/Perplex.ContentBlocks/9def8172dc17395b386dd2b558dd011e88ba2099/pkg/icon.png -------------------------------------------------------------------------------- /pkg/pack.ps1: -------------------------------------------------------------------------------- 1 | $configuration = "Release" 2 | 3 | Write-Host "Packing Perplex.ContentBlocks.StaticAssets ..." 4 | dotnet pack ..\src\Perplex.ContentBlocks.StaticAssets\Perplex.ContentBlocks.StaticAssets.csproj -c $configuration -o . 5 | 6 | Write-Host "Packing Perplex.ContentBlocks.Core ..." 7 | dotnet pack ..\src\Perplex.ContentBlocks.Core\Perplex.ContentBlocks.Core.csproj -c $configuration -o . 8 | 9 | Write-Host "Packing Perplex.ContentBlocks ..." 10 | dotnet pack ..\src\Perplex.ContentBlocks\Perplex.ContentBlocks.csproj -c $configuration -o . 11 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | trim_trailing_whitespace = true 3 | 4 | [*.cs] 5 | # Private fields are prefixed width underscore 6 | # From: https://github.com/dotnet/roslyn/issues/22884#issuecomment-358776444 7 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 8 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 9 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 10 | 11 | dotnet_naming_symbols.private_fields.applicable_kinds = field 12 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 13 | 14 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 15 | dotnet_naming_style.prefix_underscore.required_prefix = _ 16 | 17 | # Constants are UPPERCASE 18 | # From: https://stackoverflow.com/a/57403780 19 | dotnet_naming_rule.constants_should_be_upper_case.severity = suggestion 20 | dotnet_naming_rule.constants_should_be_upper_case.symbols = constants 21 | dotnet_naming_rule.constants_should_be_upper_case.style = constant_style 22 | 23 | dotnet_naming_symbols.constants.applicable_kinds = field, local 24 | dotnet_naming_symbols.constants.required_modifiers = const 25 | 26 | dotnet_naming_style.constant_style.capitalization = all_upper 27 | dotnet_naming_style.constant_style.word_separator = _ 28 | 29 | [{appSettings*.json,*.csproj}] 30 | indent_style = space 31 | indent_size = 2 32 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | True 8 | embedded 9 | NU1903 10 | 11 | 12 | 13 | 3.0.0 14 | false 15 | Perplex Digital 16 | $(Company) 17 | © $(Company) 18 | https://github.com/PerplexDigital/Perplex.ContentBlocks 19 | icon.png 20 | 21 | https://github.com/PerplexDigital/Perplex.ContentBlocks 22 | git 23 | MIT 24 | $(NoWarn);CS1591 25 | 26 | 27 | 28 | 29 | True 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/Constants.Categories.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks; 2 | 3 | public static partial class Constants 4 | { 5 | public static partial class Categories 6 | { 7 | public static readonly Guid Headers = new Guid("0a893d02-9166-437d-90e8-264524f410c0"); 8 | public static readonly Guid Content = new Guid("337cdd43-b90f-4aee-82f4-3c991b3cb8eb"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/ContentBlockCategory.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Categories; 2 | 3 | public class ContentBlockCategory : IContentBlockCategory 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = ""; 7 | public string Icon { get; set; } = ""; 8 | public bool IsHidden { get; set; } 9 | public bool IsEnabledForHeaders { get; set; } 10 | public bool IsDisabledForBlocks { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/ContentBlockCategoryComposer.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Composing; 2 | using Umbraco.Cms.Core.DependencyInjection; 3 | using Umbraco.Extensions; 4 | 5 | namespace Perplex.ContentBlocks.Categories; 6 | 7 | public class ContentBlockCategoriesComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddUnique(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/IContentBlockCategory.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Categories; 2 | 3 | public interface IContentBlockCategory 4 | { 5 | Guid Id { get; } 6 | string Name { get; } 7 | string Icon { get; } 8 | 9 | /// 10 | /// Hidden categories do not show up in the UI 11 | /// 12 | bool IsHidden { get; } 13 | 14 | /// 15 | /// Enables this category when selecting a header 16 | /// 17 | bool IsEnabledForHeaders { get; } 18 | 19 | /// 20 | /// Disables this category when selecting a block 21 | /// 22 | bool IsDisabledForBlocks { get; } 23 | } 24 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/IContentBlockCategoryRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Categories; 2 | 3 | public interface IContentBlockCategoryRepository 4 | { 5 | void Add(IContentBlockCategory category); 6 | 7 | void Remove(Guid id); 8 | 9 | IContentBlockCategory? GetById(Guid id); 10 | 11 | IEnumerable GetAll(bool includeHidden); 12 | } 13 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Categories/InMemoryContentBlockCategoryRepository.cs: -------------------------------------------------------------------------------- 1 | using static Perplex.ContentBlocks.Constants.PropertyEditor; 2 | using categories = Perplex.ContentBlocks.Constants.Categories; 3 | 4 | namespace Perplex.ContentBlocks.Categories; 5 | 6 | public class InMemoryContentBlockCategoryRepository : IContentBlockCategoryRepository 7 | { 8 | private readonly IDictionary _categories = new IContentBlockCategory[] 9 | { 10 | new ContentBlockCategory 11 | { 12 | Id = categories.Headers, 13 | Name = "Headers", 14 | Icon = $"{AssetsFolder}/icons.svg#icon-cat-header", 15 | IsEnabledForHeaders = true, 16 | IsDisabledForBlocks = true, 17 | }, 18 | 19 | new ContentBlockCategory 20 | { 21 | Id = categories.Content, 22 | Name = "Content", 23 | Icon = $"{AssetsFolder}/icons.svg#icon-cat-content", 24 | }, 25 | }.ToDictionary(d => d.Id); 26 | 27 | public IContentBlockCategory? GetById(Guid id) 28 | { 29 | return _categories.TryGetValue(id, out var definition) ? definition : null; 30 | } 31 | 32 | public IEnumerable GetAll(bool includeHidden) 33 | { 34 | var categories = _categories.Values; 35 | if (includeHidden) 36 | { 37 | return categories; 38 | } 39 | else 40 | { 41 | return categories.Where(category => !category.IsHidden); 42 | } 43 | } 44 | 45 | public void Add(IContentBlockCategory category) 46 | => _categories[category.Id] = category; 47 | 48 | public void Remove(Guid id) 49 | => _categories.Remove(id); 50 | } 51 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlockDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Definitions; 2 | 3 | public class ContentBlockDefinition : IContentBlockDefinition 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = ""; 7 | public string Description { get; set; } = ""; 8 | public string PreviewImage { get; set; } = ""; 9 | public int? DataTypeId { get; set; } 10 | public Guid? DataTypeKey { get; set; } 11 | 12 | public IEnumerable CategoryIds { get; set; } 13 | = Array.Empty(); 14 | 15 | public IEnumerable Layouts { get; set; } 16 | = Array.Empty(); 17 | 18 | public virtual IEnumerable LimitToDocumentTypes { get; set; } 19 | = Array.Empty(); 20 | 21 | public virtual IEnumerable LimitToCultures { get; set; } 22 | = Array.Empty(); 23 | } 24 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlockDefinitionComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | using Umbraco.Extensions; 5 | 6 | namespace Perplex.ContentBlocks.Definitions; 7 | 8 | public class ContentBlockDefinitionComposer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | builder.Services.AddUnique(); 13 | builder.Services.AddSingleton(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlockDefinitionFilterer.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Providers; 2 | 3 | namespace Perplex.ContentBlocks.Definitions; 4 | 5 | public class ContentBlockDefinitionFilterer : IContentBlockDefinitionFilterer 6 | { 7 | private readonly IDocumentTypeAliasProvider _documentTypeAliasProvider; 8 | 9 | public ContentBlockDefinitionFilterer(IDocumentTypeAliasProvider documentTypeAliasProvider) 10 | { 11 | _documentTypeAliasProvider = documentTypeAliasProvider; 12 | } 13 | 14 | public IEnumerable FilterForCulture(IEnumerable definitions, string culture) 15 | => definitions.Where(definition => 16 | definition.LimitToCultures?.Any() != true || 17 | definition.LimitToCultures.Any(definitionCulture => string.Equals(definitionCulture, culture, StringComparison.InvariantCultureIgnoreCase))); 18 | 19 | public IEnumerable FilterForDocumentType(IEnumerable definitions, string documentType) 20 | => definitions.Where(definition => 21 | definition.LimitToDocumentTypes?.Any() != true || 22 | definition.LimitToDocumentTypes.Any(definitionDocumentType => string.Equals(definitionDocumentType, documentType, StringComparison.InvariantCultureIgnoreCase))); 23 | 24 | public IEnumerable FilterForCultureAndDocumentType(IEnumerable definitions, string culture, string documentType) 25 | => FilterForCulture(FilterForDocumentType(definitions, documentType), culture); 26 | 27 | public IEnumerable FilterForPage(IEnumerable definitions, int pageId, string culture) 28 | { 29 | if (string.IsNullOrEmpty(culture)) 30 | { 31 | // A document type can be culture invariant, in that case nothing is filtered. 32 | return definitions; 33 | } 34 | 35 | var documentType = _documentTypeAliasProvider.GetDocumentTypeAlias(pageId); 36 | return FilterForPage(definitions, documentType, culture); 37 | } 38 | 39 | public IEnumerable FilterForPage(IEnumerable definitions, string? documentType, string culture) 40 | { 41 | if (string.IsNullOrEmpty(documentType)) 42 | { 43 | return definitions; 44 | } 45 | 46 | return FilterForCultureAndDocumentType( 47 | definitions, 48 | culture, 49 | documentType); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlockDefinition{T}.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Perplex.ContentBlocks.Definitions; 4 | 5 | public class ContentBlockDefinition : IContentBlockDefinition where T : ViewComponent 6 | { 7 | public Guid Id { get; set; } 8 | public string Name { get; set; } = ""; 9 | public string Description { get; set; } = ""; 10 | public string PreviewImage { get; set; } = ""; 11 | public int? DataTypeId { get; set; } 12 | public Guid? DataTypeKey { get; set; } 13 | 14 | public IEnumerable CategoryIds { get; set; } 15 | = Array.Empty(); 16 | 17 | public IEnumerable Layouts { get; set; } 18 | = Array.Empty(); 19 | 20 | public virtual IEnumerable LimitToDocumentTypes { get; set; } 21 | = Array.Empty(); 22 | 23 | public virtual IEnumerable LimitToCultures { get; set; } 24 | = Array.Empty(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlockLayout.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Definitions; 2 | 3 | public class ContentBlockLayout : IContentBlockLayout 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = ""; 7 | public string? Description { get; set; } 8 | public string? PreviewImage { get; set; } 9 | public string? ViewPath { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/ContentBlocksDefinitionApiController.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Categories; 2 | using System.Collections.Generic; 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json; 7 | using System.Net.Mime; 8 | using Umbraco.Cms.Web.BackOffice.Controllers; 9 | 10 | namespace Perplex.ContentBlocks.Definitions; 11 | 12 | public class ContentBlocksDefinitionApiController : UmbracoAuthorizedApiController 13 | { 14 | private readonly IContentBlockDefinitionRepository _definitionRepository; 15 | private readonly IContentBlockCategoryRepository _categoryRepository; 16 | 17 | public ContentBlocksDefinitionApiController(IContentBlockDefinitionRepository definitionRepository, IContentBlockCategoryRepository categoryRepository) 18 | { 19 | _definitionRepository = definitionRepository; 20 | _categoryRepository = categoryRepository; 21 | } 22 | 23 | [HttpGet] 24 | public IActionResult GetAllDefinitions() 25 | { 26 | var definitions = _definitionRepository.GetAll(); 27 | return JsonContent(definitions); 28 | } 29 | 30 | [HttpGet] 31 | public IActionResult GetAllCategories() 32 | { 33 | var categories = _categoryRepository.GetAll(true); 34 | return JsonContent(categories); 35 | } 36 | 37 | [HttpGet] 38 | public IActionResult GetDefinitionsForPage(string documentType, string culture) 39 | { 40 | var definitions = _definitionRepository.GetAllForPage(documentType, culture); 41 | return JsonContent(definitions); 42 | } 43 | 44 | /// 45 | /// Returns obj serialized to JSON using Newtonsoft.Json. 46 | /// The Json serializer + deserializer in .NET Core has been switched 47 | /// to the System.Text.Json namespace which handles casing differently. 48 | /// It will transform properties to camelCase by default instead of using the original casing. 49 | /// We used PascalCasing in v8 and want to continue to do so to prevent having to rewrite the front-end code, 50 | /// so we explicitly use Newtonsoft here to create the JSON response by hand. 51 | /// 52 | /// Object to serialize to JSON 53 | /// 54 | private IActionResult JsonContent(object obj) 55 | { 56 | var serialized = JsonConvert.SerializeObject(obj); 57 | return Content(serialized, MediaTypeNames.Application.Json); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/IContentBlockDefinition.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models; 2 | 3 | namespace Perplex.ContentBlocks.Definitions; 4 | 5 | public interface IContentBlockDefinition 6 | { 7 | /// 8 | /// Unique id of this Content Block definition 9 | /// 10 | Guid Id { get; } 11 | 12 | /// 13 | /// Name of this Content Block definition 14 | /// 15 | string Name { get; } 16 | 17 | /// 18 | /// Description of this Content Block definition 19 | /// 20 | string Description { get; } 21 | 22 | /// 23 | /// Preview image that will appear in the backoffice UI when selecting blocks 24 | /// 25 | string PreviewImage { get; } 26 | 27 | /// 28 | /// Data type id of the Nested Content data type used for this Content Block definition. 29 | /// Provide either DataTypeId OR DataTypeKey, not both. Leave one of them NULL. 30 | /// 31 | [Obsolete("Use " + nameof(DataTypeKey) + " instead. This will be removed in a next major release.")] 32 | int? DataTypeId { get; } 33 | 34 | /// 35 | /// Data type key of the Nested Content data type used for this Content Block definition. 36 | /// Provide either DataTypeId OR DataTypeKey, not both. Leave one of them NULL. 37 | /// 38 | Guid? DataTypeKey { get; } 39 | 40 | /// 41 | /// Category ids this definition belongs to. 42 | /// 43 | IEnumerable CategoryIds { get; } 44 | 45 | /// 46 | /// Layouts this block defines. Make sure to specify at least one layout. 47 | /// 48 | IEnumerable Layouts { get; } 49 | 50 | /// 51 | /// Limits this Content Block definition to only the given document types. 52 | /// Specify the document type alias (). 53 | /// When configured, this Content Block definition will only show up in the Backoffice UI 54 | /// on the specified document types. 55 | /// 56 | IEnumerable LimitToDocumentTypes { get; } 57 | 58 | /// 59 | /// Limits this Content Block definition to only the given cultures. 60 | /// When configured, this Content Block definition will only show up in the Backoffice UI 61 | /// on when editing a specific culture. 62 | /// 63 | IEnumerable LimitToCultures { get; } 64 | } 65 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/IContentBlockDefinitionFilterer.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Definitions; 2 | 3 | public interface IContentBlockDefinitionFilterer 4 | { 5 | IEnumerable FilterForCulture(IEnumerable definitions, string culture); 6 | 7 | IEnumerable FilterForDocumentType(IEnumerable definitions, string documentType); 8 | 9 | IEnumerable FilterForCultureAndDocumentType(IEnumerable definitions, 10 | string culture, string documentType); 11 | 12 | IEnumerable FilterForPage(IEnumerable definitions, int pageId, string culture); 13 | 14 | IEnumerable FilterForPage(IEnumerable definitions, string documentType, string culture); 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/IContentBlockDefinitionRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Definitions; 2 | 3 | public interface IContentBlockDefinitionRepository 4 | { 5 | void Add(IContentBlockDefinition definition); 6 | 7 | void Remove(Guid id); 8 | 9 | IContentBlockDefinition? GetById(Guid id); 10 | 11 | IEnumerable GetAll(); 12 | 13 | IEnumerable GetAllForPage(int pageId, string culture); 14 | 15 | IEnumerable GetAllForPage(string documentType, string culture); 16 | } 17 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/IContentBlockDefinition{T}.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Perplex.ContentBlocks.Definitions; 4 | 5 | public interface IContentBlockDefinition : IContentBlockDefinition where T : ViewComponent 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/IContentBlockLayout.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Rendering; 2 | 3 | namespace Perplex.ContentBlocks.Definitions; 4 | 5 | public interface IContentBlockLayout 6 | { 7 | Guid Id { get; } 8 | string Name { get; } 9 | string? Description { get; } 10 | string? PreviewImage { get; } 11 | 12 | /// 13 | /// Full path to the View file of this ContentBlockLayout, 14 | /// e.g. "~/Views/Partials/ContentBlocks/ExampleBlock.cshtml". 15 | /// When using that uses a view component 16 | /// this property is optional since your defined view component will be responsible for rendering. 17 | /// The ViewPath will be called with model . 18 | /// 19 | string? ViewPath { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Definitions/InMemoryContentBlockDefinitionRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Definitions; 2 | 3 | public class InMemoryContentBlockDefinitionRepository : IContentBlockDefinitionRepository 4 | { 5 | public InMemoryContentBlockDefinitionRepository(IContentBlockDefinitionFilterer definitionFilterer) 6 | { 7 | _definitionFilterer = definitionFilterer; 8 | _definitions = new Dictionary(); 9 | } 10 | 11 | private readonly IDictionary _definitions; 12 | 13 | private readonly IContentBlockDefinitionFilterer _definitionFilterer; 14 | 15 | public IContentBlockDefinition? GetById(Guid id) 16 | => _definitions.TryGetValue(id, out var definition) ? definition : null; 17 | 18 | public IEnumerable GetAll() 19 | { 20 | return _definitions.Values; 21 | } 22 | 23 | public IEnumerable GetAllForPage(int pageId, string culture) 24 | => _definitionFilterer.FilterForPage(GetAll(), pageId, culture); 25 | 26 | public IEnumerable GetAllForPage(string documentType, string culture) 27 | => _definitionFilterer.FilterForPage(GetAll(), documentType, culture); 28 | 29 | public void Add(IContentBlockDefinition definition) 30 | => _definitions[definition.Id] = definition; 31 | 32 | public void Remove(Guid id) 33 | => _definitions.Remove(id); 34 | } 35 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Perplex.ContentBlocks.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | True 5 | False 6 | 7 | 8 | 9 | Perplex.ContentBlocks.Core 10 | Perplex.ContentBlocks assembly only 11 | umbraco 12 | Perplex.ContentBlocks 13 | Perplex.ContentBlocks 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/ContentBlockPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public class ContentBlockPreset : IContentBlockPreset 4 | { 5 | public Guid Id { get; set; } 6 | public Guid DefinitionId { get; set; } 7 | public Guid LayoutId { get; set; } 8 | public bool IsMandatory { get; set; } 9 | 10 | public IDictionary Values { get; set; } 11 | = new Dictionary(); 12 | 13 | public IEnumerable Variants { get; set; } 14 | = Enumerable.Empty(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/ContentBlockPresetsComposer.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Composing; 2 | using Umbraco.Cms.Core.DependencyInjection; 3 | using Umbraco.Extensions; 4 | 5 | namespace Perplex.ContentBlocks.Presets; 6 | 7 | public class ContentBlockPresetsComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddUnique(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/ContentBlockVariantPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public class ContentBlockVariantPreset : IContentBlockVariantPreset 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Alias { get; set; } = ""; 8 | 9 | /// 10 | /// The initial values of the generated Content Block variant per property alias of the IPublishedElement 11 | /// 12 | public IDictionary Values { get; set; } 13 | = new Dictionary(); 14 | } 15 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/ContentBlocksPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public class ContentBlocksPreset : IContentBlocksPreset 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = ""; 7 | 8 | public IEnumerable ApplyToCultures { get; set; } 9 | = Enumerable.Empty(); 10 | 11 | public IEnumerable ApplyToDocumentTypes { get; set; } 12 | = Enumerable.Empty(); 13 | 14 | public IContentBlockPreset? Header { get; set; } 15 | 16 | public IEnumerable Blocks { get; set; } 17 | = Enumerable.Empty(); 18 | } 19 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/ContentBlocksPresetApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Perplex.ContentBlocks.Presets; 3 | using Umbraco.Cms.Web.BackOffice.Controllers; 4 | 5 | namespace Perplex.ContentBlocks.Definitions; 6 | 7 | public class ContentBlocksPresetApiController : UmbracoAuthorizedApiController 8 | { 9 | private readonly IContentBlocksPresetRepository _presetRepository; 10 | 11 | public ContentBlocksPresetApiController(IContentBlocksPresetRepository presetRepository) 12 | { 13 | _presetRepository = presetRepository; 14 | } 15 | 16 | [HttpGet] 17 | public IEnumerable GetAllPresets() 18 | => _presetRepository.GetAll(); 19 | 20 | [HttpGet] 21 | public IContentBlocksPreset? GetPresetForPage(string documentType, string culture) 22 | => _presetRepository.GetPresetForPage(documentType, culture); 23 | } 24 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/IContentBlockPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public interface IContentBlockPreset 4 | { 5 | /// 6 | /// Unique id of this preset. 7 | /// 8 | Guid Id { get; } 9 | 10 | /// 11 | /// Definition to use for this preset. 12 | /// 13 | Guid DefinitionId { get; } 14 | 15 | /// 16 | /// Layout to use for this preset. 17 | /// 18 | Guid LayoutId { get; } 19 | 20 | /// 21 | /// When set to true, the block specified by this preset cannot be hidden or removed. 22 | /// 23 | bool IsMandatory { get; } 24 | 25 | /// 26 | /// The initial values of the generated Content Block per property alias of the IPublishedElement. 27 | /// 28 | IDictionary Values { get; } 29 | 30 | /// 31 | /// The variants of this preset. 32 | /// 33 | IEnumerable Variants { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/IContentBlockVariantPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public interface IContentBlockVariantPreset 4 | { 5 | /// 6 | /// Unique id of this variant preset 7 | /// 8 | Guid Id { get; } 9 | 10 | /// 11 | /// The variant alias 12 | /// 13 | string Alias { get; } 14 | 15 | /// 16 | /// The initial values of the generated Content Block variant per property alias of the IPublishedElement 17 | /// 18 | IDictionary Values { get; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/IContentBlocksPreset.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public interface IContentBlocksPreset 4 | { 5 | Guid Id { get; } 6 | string Name { get; } 7 | 8 | IEnumerable ApplyToCultures { get; } 9 | IEnumerable ApplyToDocumentTypes { get; } 10 | 11 | IContentBlockPreset? Header { get; } 12 | IEnumerable Blocks { get; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/IContentBlocksPresetRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Presets; 2 | 3 | public interface IContentBlocksPresetRepository 4 | { 5 | void Add(IContentBlocksPreset preset); 6 | 7 | void Remove(Guid id); 8 | 9 | IEnumerable GetAll(); 10 | 11 | IContentBlocksPreset? GetPresetForPage(int pageId, string culture); 12 | 13 | IContentBlocksPreset? GetPresetForPage(string documentType, string culture); 14 | } 15 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Presets/InMemoryContentBlocksPresetRepository.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Providers; 2 | 3 | namespace Perplex.ContentBlocks.Presets; 4 | 5 | public class InMemoryContentBlocksPresetRepository : IContentBlocksPresetRepository 6 | { 7 | private readonly IDocumentTypeAliasProvider _documentTypeAliasProvider; 8 | 9 | public InMemoryContentBlocksPresetRepository(IDocumentTypeAliasProvider documentTypeAliasProvider) 10 | { 11 | _documentTypeAliasProvider = documentTypeAliasProvider; 12 | } 13 | 14 | private readonly IDictionary _presets = new Dictionary(); 15 | 16 | public void Add(IContentBlocksPreset preset) 17 | => _presets[preset.Id] = preset; 18 | 19 | public void Remove(Guid id) 20 | => _presets.Remove(id); 21 | 22 | public IContentBlocksPreset? GetById(Guid id) 23 | { 24 | return _presets.TryGetValue(id, out var preset) ? preset : null; 25 | } 26 | 27 | public IEnumerable GetAll() 28 | { 29 | return _presets.Values; 30 | } 31 | 32 | public IContentBlocksPreset? GetPresetForPage(int pageId, string culture) 33 | { 34 | if (string.IsNullOrEmpty(culture)) 35 | { 36 | return null; 37 | } 38 | 39 | var documentType = _documentTypeAliasProvider.GetDocumentTypeAlias(pageId); 40 | if (documentType is null) return null; 41 | 42 | return GetPresetForPage(documentType, culture); 43 | } 44 | 45 | public IContentBlocksPreset? GetPresetForPage(string documentType, string culture) 46 | { 47 | if (string.IsNullOrEmpty(documentType)) 48 | { 49 | return null; 50 | } 51 | 52 | bool isEmptyOrContains(IEnumerable input, string toMatch) 53 | => input?.Any() != true || input.Any(i => string.Equals(i, toMatch, StringComparison.InvariantCultureIgnoreCase)); 54 | 55 | return GetAll()?.FirstOrDefault(p => 56 | isEmptyOrContains(p.ApplyToCultures, culture) && 57 | isEmptyOrContains(p.ApplyToDocumentTypes, documentType)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/Constants.Preview.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks; 2 | 3 | public static partial class Constants 4 | { 5 | public static partial class Preview 6 | { 7 | public const string UmbracoCookieName = "UMB_UCONTEXT"; 8 | public const string UmbracoPreviewCookieName = "UMB_PREVIEW"; 9 | public const string UmbracoPreviewCookieValue = "preview"; 10 | public const string UmbracoPreviewPath = "umbraco/preview/frame"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/ContentBlocksPreviewApiController.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Perplex.ContentBlocks.Utils.Cookies; 4 | using System.Net.Mime; 5 | using System.Text; 6 | using Umbraco.Cms.Web.BackOffice.Controllers; 7 | using static Perplex.ContentBlocks.Constants.Preview; 8 | 9 | namespace Perplex.ContentBlocks.Preview; 10 | 11 | public class ContentBlocksPreviewApiController : UmbracoAuthorizedApiController 12 | { 13 | private static readonly HttpClient _httpClient; 14 | private readonly IPreviewScrollScriptProvider _scrollScriptProvider; 15 | private readonly IHttpCookiesAccessor _httpCookiesAccessor; 16 | 17 | static ContentBlocksPreviewApiController() 18 | { 19 | var handler = new HttpClientHandler 20 | { 21 | UseCookies = false, 22 | // Do not validate any certificates, which fails on self signed certificates and causes the preview to fail. 23 | ServerCertificateCustomValidationCallback = (_, __, ___, ____) => true 24 | }; 25 | 26 | _httpClient = new HttpClient(handler); 27 | } 28 | 29 | public ContentBlocksPreviewApiController(IPreviewScrollScriptProvider scrollScriptProvider, IHttpCookiesAccessor httpCookiesAccessor) 30 | { 31 | _scrollScriptProvider = scrollScriptProvider; 32 | _httpCookiesAccessor = httpCookiesAccessor; 33 | } 34 | 35 | [HttpGet] 36 | public async Task GetPreviewForIframe(int? pageId, string culture) 37 | { 38 | string html = string.Empty; 39 | 40 | if (pageId != null) 41 | { 42 | html = await GetPreviewHtml(pageId.Value, culture); 43 | } 44 | 45 | return Content(html, MediaTypeNames.Text.Html, Encoding.UTF8); 46 | } 47 | 48 | private async Task GetPreviewHtml(int pageId, string culture) 49 | { 50 | string host = Request.Scheme + "://" + Request.Host; 51 | string path = GetPreviewPath(pageId, culture); 52 | 53 | if (!Uri.TryCreate($"{host}/{path}", UriKind.Absolute, out Uri? previewUri)) 54 | { 55 | return string.Empty; 56 | } 57 | 58 | var message = new HttpRequestMessage(HttpMethod.Get, previewUri); 59 | 60 | IList cookies = new List(); 61 | if (_httpCookiesAccessor.Cookies.TryGetValue(UmbracoCookieName, out string? umbracoCookieValue)) 62 | { 63 | cookies.Add($"{UmbracoCookieName}={umbracoCookieValue}"); 64 | } 65 | 66 | // Preview cookie to enable preview mode 67 | cookies.Add($"{UmbracoPreviewCookieName}={UmbracoPreviewCookieValue}"); 68 | 69 | string cookieHeader = string.Join("; ", cookies); 70 | message.Headers.Add("Cookie", cookieHeader); 71 | 72 | var result = await _httpClient.SendAsync(message); 73 | string html = await result.Content.ReadAsStringAsync(); 74 | 75 | if (!result.IsSuccessStatusCode) 76 | { 77 | // Do not modify the HTML when it was not successful 78 | return html; 79 | } 80 | else 81 | { 82 | return TransformOriginalHtmlToPreviewHtml(html); 83 | } 84 | } 85 | 86 | /// 87 | /// Transforms the original HTML we receive from Umbraco's preview mode to the format we want. 88 | /// 89 | /// Original HTML of this page based on Umbraco's preview mode 90 | /// 91 | private string TransformOriginalHtmlToPreviewHtml(string originalHtml) 92 | { 93 | if (string.IsNullOrEmpty(originalHtml)) 94 | { 95 | return originalHtml; 96 | } 97 | 98 | HtmlDocument doc = new HtmlDocument(); 99 | doc.LoadHtml(originalHtml); 100 | 101 | if (doc.DocumentNode.SelectSingleNode("//*[@id='umbracoPreviewBadge']") is HtmlNode previewLabel) 102 | { 103 | // Remove Umbraco Preview Badge 104 | previewLabel.Remove(); 105 | } 106 | 107 | if (doc.DocumentNode.SelectSingleNode("//body") is HtmlNode body) 108 | { 109 | AppendScrollScript(body); 110 | } 111 | 112 | // .OuterHtml does not contain a DOCTYPE declaration so we add this manually 113 | return "" + doc.DocumentNode.OuterHtml; 114 | } 115 | 116 | private void AppendScrollScript(HtmlNode parent) 117 | { 118 | string script = @" 119 | "; 130 | 131 | var scriptNode = HtmlNode.CreateNode(script); 132 | parent.AppendChild(scriptNode); 133 | } 134 | 135 | private static string GetPreviewPath(int pageId, string culture) 136 | { 137 | string path = UmbracoPreviewPath + "?id=" + pageId; 138 | if (!string.IsNullOrEmpty(culture)) 139 | { 140 | // If a content type is not set to "allow varying by culture" 141 | // the culture will be null. 142 | path += "&culture=" + culture; 143 | } 144 | 145 | return path; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/ContentBlocksPreviewComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | using Umbraco.Extensions; 5 | 6 | namespace Perplex.ContentBlocks.Preview; 7 | 8 | public class ContentBlocksPreviewComposer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | builder.Services.AddScoped(); 13 | 14 | // Can be replaced by clients 15 | builder.Services.AddUnique(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/CookieBasedPreviewModeProvider.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Utils.Cookies; 2 | 3 | namespace Perplex.ContentBlocks.Preview; 4 | 5 | public class CookieBasedPreviewModeProvider : IPreviewModeProvider 6 | { 7 | private readonly IHttpCookiesAccessor _cookiesAccessor; 8 | 9 | public CookieBasedPreviewModeProvider(IHttpCookiesAccessor cookiesAccessor) 10 | { 11 | _cookiesAccessor = cookiesAccessor; 12 | } 13 | 14 | public bool IsPreviewMode => 15 | _cookiesAccessor.Cookies.TryGetValue(Constants.Preview.UmbracoPreviewCookieName, out string? value) && 16 | value == Constants.Preview.UmbracoPreviewCookieValue; 17 | } 18 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/DefaultPreviewScrollScriptProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Preview; 2 | 3 | /// 4 | /// Default preview scroll script. 5 | /// Scrolls exactly to the anchor generated for a Content Block. 6 | /// Does not take into account a fixed header bar or other elements like that. 7 | /// 8 | public class DefaultPreviewScrollScriptProvider : IPreviewScrollScriptProvider 9 | { 10 | private const string SCROLL_SCRIPT = @" 11 | if (typeof window.scrollTo === ""function"") { 12 | window.scrollTo({ 13 | top: element.offsetTop, 14 | behavior: ""smooth"" 15 | }); 16 | }"; 17 | 18 | public string ScrollScript { get; } = SCROLL_SCRIPT; 19 | } 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/IPreviewModeProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Preview; 2 | 3 | public interface IPreviewModeProvider 4 | { 5 | /// 6 | /// Indicates if ContentBlocks are being rendered in the back office preview window. 7 | /// 8 | bool IsPreviewMode { get; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Preview/IPreviewScrollScriptProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Preview; 2 | 3 | public interface IPreviewScrollScriptProvider 4 | { 5 | /// 6 | /// JavaScript function to execute when scrolling to a specific Content Block. 7 | /// The JavaScript provided will be executed within a function scope that 8 | /// has already set an "element" variable to the DOM element of the scroll anchor. 9 | /// 10 | string ScrollScript { get; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/Configuration/Constants.PropertyEditor.Configuration.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks; 2 | 3 | public static partial class Constants 4 | { 5 | public static partial class PropertyEditor 6 | { 7 | public static partial class Configuration 8 | { 9 | public const string VersionKey = "_version"; 10 | 11 | public const string StructureKey = "structure"; 12 | public const string StructureViewName = "/App_Plugins/Perplex.ContentBlocks/configuration/perplex.content-blocks.configuration.structure-view.html"; 13 | 14 | public const string DisablePreviewKey = "disablePreview"; 15 | public const string DisablePreviewViewName = "boolean"; 16 | 17 | public const string HideLabelKey = "hideLabel"; 18 | public const string HideLabelViewName = "boolean"; 19 | 20 | public const string HidePropertyGroupContainerKey = "hidePropertyGroupContainer"; 21 | public const string HidePropertyGroupContainerViewName = "boolean"; 22 | 23 | public const string AllowBlocksWithoutHeaderKey = "allowBlocksWithoutHeader"; 24 | public const string AllowBlocksWithoutHeaderViewName = "boolean"; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/Configuration/ContentBlocksConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.PropertyEditor.Configuration; 2 | 3 | /// 4 | /// The structure of the editor in the backoffice 5 | /// 6 | [Flags] 7 | public enum Structure 8 | { 9 | /// 10 | /// Default value - nothing 11 | /// 12 | None = 0, 13 | 14 | /// 15 | /// Blocks only 16 | /// 17 | Blocks = 1, 18 | 19 | /// 20 | /// Header only 21 | /// 22 | Header = 2, 23 | 24 | /// 25 | /// Blocks and Header 26 | /// 27 | All = Blocks | Header, 28 | } 29 | 30 | /// 31 | /// ContentBlocks configuration 32 | /// 33 | public class ContentBlocksConfiguration 34 | { 35 | /// 36 | /// Added for detecting out of date configuration objects in the future. 37 | /// 38 | public int Version { get; set; } 39 | 40 | /// 41 | /// The structure of the editor in the backoffice 42 | /// 43 | public Structure Structure { get; set; } 44 | 45 | /// 46 | /// Indicates if the preview component should be hidden 47 | /// 48 | public bool DisablePreview { get; set; } 49 | 50 | /// 51 | /// Indicates if the label of the editor should be hidden 52 | /// 53 | public bool HideLabel { get; set; } 54 | 55 | /// 56 | /// Indicates if the property group container should be hidden 57 | /// 58 | public bool HidePropertyGroupContainer { get; set; } 59 | 60 | /// 61 | /// Indicates if it is allowed to add blocks without first setting a header 62 | /// 63 | public bool AllowBlocksWithoutHeader { get; set; } 64 | 65 | /// 66 | /// Current configuration version. 67 | /// 68 | public const int VERSION = 3; 69 | 70 | public static readonly ContentBlocksConfiguration DefaultConfiguration = new() 71 | { 72 | Version = VERSION, 73 | 74 | HideLabel = true, 75 | Structure = Structure.Blocks | Structure.Header, 76 | DisablePreview = false, 77 | 78 | // It is quite likely this will default to "false" in the future 79 | // considering hiding the property group container is messing with 80 | // the default Umbraco UI and also causes some flickering upon page load 81 | // when the group is being hidden after our editor is initialized. 82 | HidePropertyGroupContainer = true, 83 | 84 | AllowBlocksWithoutHeader = false, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/Constants.PropertyEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Perplex.ContentBlocks; 4 | 5 | public static partial class Constants 6 | { 7 | public static partial class PropertyEditor 8 | { 9 | public const string Alias = "Perplex.ContentBlocks"; 10 | public const string Name = "Perplex.ContentBlocks"; 11 | 12 | /// 13 | /// The view path to the main ContentBlocks HTML, including a cache buster when the product version can be read from the DLL. 14 | /// 15 | public static readonly string ViewPath = "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.html" + (GetProductVersion() is string version ? "?v=" + version : ""); 16 | 17 | public const string AssetsFolder = "/App_Plugins/Perplex.ContentBlocks/assets"; 18 | 19 | private static string? GetProductVersion() 20 | { 21 | try 22 | { 23 | if (typeof(PropertyEditor).Assembly.Location is string assemblyLocation && 24 | !string.IsNullOrWhiteSpace(assemblyLocation) && 25 | FileVersionInfo.GetVersionInfo(assemblyLocation).ProductVersion is string productVersion && 26 | !string.IsNullOrWhiteSpace(productVersion)) 27 | { 28 | return productVersion; 29 | } 30 | } 31 | catch 32 | { 33 | // Ignore 34 | } 35 | 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ContentBlocksPropertyEditor.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor.Configuration; 2 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 3 | using Perplex.ContentBlocks.Utils; 4 | using Umbraco.Cms.Core.IO; 5 | using Umbraco.Cms.Core.Models; 6 | using Umbraco.Cms.Core.PropertyEditors; 7 | using Umbraco.Cms.Core.Serialization; 8 | using Umbraco.Cms.Core.Services; 9 | using Umbraco.Cms.Core.Strings; 10 | 11 | namespace Perplex.ContentBlocks.PropertyEditor; 12 | 13 | public class ContentBlocksPropertyEditor : IDataEditor 14 | { 15 | private readonly ContentBlocksModelValueDeserializer _deserializer; 16 | private readonly ContentBlockUtils _utils; 17 | 18 | private readonly IIOHelper _iOHelper; 19 | private readonly ILocalizedTextService _localizedTextService; 20 | private readonly IShortStringHelper _shortStringHelper; 21 | private readonly IJsonSerializer _jsonSerializer; 22 | private readonly IPropertyValidationService _validationService; 23 | private readonly IEditorConfigurationParser _editorConfigurationParser; 24 | 25 | public ContentBlocksPropertyEditor( 26 | ContentBlocksModelValueDeserializer deserializer, 27 | ContentBlockUtils utils, 28 | IIOHelper iOHelper, 29 | ILocalizedTextService localizedTextService, 30 | IShortStringHelper shortStringHelper, 31 | IJsonSerializer jsonSerializer, 32 | IPropertyValidationService validationService, 33 | IEditorConfigurationParser editorConfigurationParser) 34 | { 35 | _deserializer = deserializer; 36 | _utils = utils; 37 | _iOHelper = iOHelper; 38 | _localizedTextService = localizedTextService; 39 | _shortStringHelper = shortStringHelper; 40 | _jsonSerializer = jsonSerializer; 41 | _validationService = validationService; 42 | _editorConfigurationParser = editorConfigurationParser; 43 | } 44 | 45 | public string Alias { get; } = Constants.PropertyEditor.Alias; 46 | public EditorType Type { get; } = EditorType.PropertyValue; 47 | public string Name { get; } = Constants.PropertyEditor.Name; 48 | 49 | // Icon cannot be NULL for Umbraco 8.6+, 50 | // it will actually crash the UI. 51 | public string Icon { get; } = "icon-list"; 52 | 53 | public string Group { get; } = "Lists"; 54 | 55 | public bool IsDeprecated { get; } = false; 56 | 57 | public IDictionary DefaultConfiguration => GetConfigurationEditor().DefaultConfiguration; 58 | 59 | public IPropertyIndexValueFactory PropertyIndexValueFactory 60 | => new DefaultPropertyIndexValueFactory(); 61 | 62 | public IConfigurationEditor GetConfigurationEditor() 63 | => new ContentBlocksConfigurationEditor(_iOHelper, _editorConfigurationParser); 64 | 65 | public IDataValueEditor GetValueEditor() 66 | => GetValueEditor(null); 67 | 68 | public IDataValueEditor GetValueEditor(object? configuration) 69 | { 70 | var validator = new ContentBlocksValidator(_deserializer, _utils, _validationService, _shortStringHelper); 71 | 72 | bool hideLabel = (configuration as ContentBlocksConfiguration)?.HideLabel 73 | ?? ContentBlocksConfiguration.DefaultConfiguration.HideLabel; 74 | 75 | return new ContentBlocksValueEditor(_deserializer, _utils, _localizedTextService, _shortStringHelper, _jsonSerializer) 76 | { 77 | View = Constants.PropertyEditor.ViewPath, 78 | Configuration = configuration, 79 | HideLabel = hideLabel, 80 | ValueType = ValueTypes.Json, 81 | Validators = { validator } 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ContentBlocksValidator.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 2 | using Perplex.ContentBlocks.Utils; 3 | 4 | using Umbraco.Cms.Core.Models; 5 | using Umbraco.Cms.Core.PropertyEditors; 6 | using Umbraco.Cms.Core.Services; 7 | using Umbraco.Cms.Core.Strings; 8 | 9 | namespace Perplex.ContentBlocks.PropertyEditor; 10 | 11 | public class ContentBlocksValidator : ComplexEditorValidator 12 | { 13 | private readonly ContentBlockUtils _utils; 14 | private readonly ContentBlocksModelValueDeserializer _deserializer; 15 | 16 | private readonly IShortStringHelper _shortStringHelper; 17 | 18 | public ContentBlocksValidator( 19 | ContentBlocksModelValueDeserializer deserializer, 20 | ContentBlockUtils utils, 21 | IPropertyValidationService validationService, 22 | IShortStringHelper shortStringHelper) : base(validationService) 23 | { 24 | _deserializer = deserializer; 25 | _utils = utils; 26 | _shortStringHelper = shortStringHelper; 27 | } 28 | 29 | protected override IEnumerable GetElementTypeValidation(object? value) 30 | { 31 | var modelValue = _deserializer.Deserialize(value?.ToString()); 32 | if (modelValue == null) 33 | { 34 | yield break; 35 | } 36 | 37 | if (modelValue.Header != null && GetValidationModel(modelValue.Header) is ElementTypeValidationModel headerValidationModel) 38 | { 39 | yield return headerValidationModel; 40 | } 41 | 42 | if (modelValue.Blocks?.Any() == true) 43 | { 44 | foreach (var block in modelValue.Blocks) 45 | { 46 | if (GetValidationModel(block) is ElementTypeValidationModel blockValidationModel) 47 | { 48 | yield return blockValidationModel; 49 | } 50 | } 51 | } 52 | 53 | ElementTypeValidationModel? GetValidationModel(ContentBlockModelValue blockValue) 54 | { 55 | IDataType? dataType = _utils.GetDataType(blockValue.DefinitionId); 56 | 57 | if (dataType is null) 58 | { 59 | return null; 60 | } 61 | 62 | var validationModel = new ElementTypeValidationModel("", blockValue.Id); 63 | 64 | var propType = new PropertyType(_shortStringHelper, dataType) { Alias = "content" }; 65 | validationModel.AddPropertyTypeValidation(new PropertyTypeValidationModel(propType, blockValue.Content?.ToString())); 66 | 67 | if (blockValue.Variants?.Any() == true) 68 | { 69 | foreach (var variant in blockValue.Variants) 70 | { 71 | var variantPropType = new PropertyType(_shortStringHelper, dataType) { Alias = "content_variant_" + variant.Id.ToString("N") }; 72 | validationModel.AddPropertyTypeValidation(new PropertyTypeValidationModel(variantPropType, variant.Content?.ToString())); 73 | } 74 | } 75 | 76 | return validationModel; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ContentBlocksValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor.Configuration; 2 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 3 | using Perplex.ContentBlocks.Rendering; 4 | using Perplex.ContentBlocks.Variants; 5 | using Umbraco.Cms.Core.Models.PublishedContent; 6 | using Umbraco.Cms.Core.PropertyEditors; 7 | using Umbraco.Cms.Core.PropertyEditors.ValueConverters; 8 | 9 | namespace Perplex.ContentBlocks.PropertyEditor; 10 | 11 | public class ContentBlocksValueConverter : PropertyValueConverterBase 12 | { 13 | private readonly NestedContentSingleValueConverter _nestedContentSingleValueConverter; 14 | private readonly ContentBlocksModelValueDeserializer _deserializer; 15 | private readonly IContentBlockVariantSelector _variantSelector; 16 | private readonly IServiceProvider _serviceProvider; 17 | 18 | public ContentBlocksValueConverter( 19 | NestedContentSingleValueConverter nestedContentSingleValueConverter, 20 | ContentBlocksModelValueDeserializer deserializer, 21 | IContentBlockVariantSelector variantSelector, 22 | IServiceProvider serviceProvider 23 | ) 24 | { 25 | _nestedContentSingleValueConverter = nestedContentSingleValueConverter; 26 | _deserializer = deserializer; 27 | _variantSelector = variantSelector; 28 | _serviceProvider = serviceProvider; 29 | } 30 | 31 | public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) 32 | { 33 | // We might be able to set this to .Elements. This ensures the cache will be refreshed 34 | // even after publishing any other content, which ensures no issues arise when the block 35 | // contains editors that reference other content (e.g. a ContentPicker). 36 | // However, this requires proper testing first with a wide range of editors. 37 | // Until that time, .Snapshot is the safest option: per request caching. 38 | return PropertyCacheLevel.Snapshot; 39 | } 40 | 41 | public override bool IsConverter(IPublishedPropertyType propertyType) 42 | => propertyType.EditorAlias == Constants.PropertyEditor.Alias; 43 | 44 | public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) 45 | { 46 | ContentBlocksModelValue? modelValue = _deserializer.Deserialize(inter?.ToString()); 47 | if (modelValue is null) 48 | { 49 | return Rendering.ContentBlocks.Empty; 50 | } 51 | 52 | var interValue = new ContentBlocksInterValue 53 | { 54 | Header = SelectBlock(modelValue.Header), 55 | Blocks = modelValue.Blocks?.Select(SelectBlock).OfType().ToArray() ?? Array.Empty(), 56 | }; 57 | 58 | var config = propertyType.DataType.ConfigurationAs() ?? ContentBlocksConfiguration.DefaultConfiguration; 59 | 60 | var header = config.Structure.HasFlag(Structure.Header) 61 | ? CreateViewModel(interValue.Header) 62 | : null; 63 | 64 | var blocks = config.Structure.HasFlag(Structure.Blocks) 65 | ? interValue.Blocks.Select(CreateViewModel).OfType().ToArray() 66 | : Array.Empty(); 67 | 68 | return new Rendering.ContentBlocks 69 | { 70 | Header = header, 71 | Blocks = blocks 72 | }; 73 | 74 | ContentBlockInterValue? SelectBlock(ContentBlockModelValue? original) 75 | { 76 | if (original is null || original.IsDisabled) 77 | { 78 | return null; 79 | } 80 | 81 | // Start with default content 82 | var block = new ContentBlockInterValue 83 | { 84 | Id = original.Id, 85 | DefinitionId = original.DefinitionId, 86 | LayoutId = original.LayoutId, 87 | Content = original.Content, 88 | }; 89 | 90 | if (_variantSelector.SelectVariant(original, owner, preview) is ContentBlockVariantModelValue variant) 91 | { 92 | // Use variant instead, note we always use the definition + layout specified by the block 93 | block.Id = variant.Id; 94 | block.Content = variant.Content; 95 | }; 96 | 97 | return block; 98 | } 99 | 100 | IContentBlockViewModel? CreateViewModel(ContentBlockInterValue? block) 101 | { 102 | if (block is null) 103 | { 104 | return null; 105 | } 106 | 107 | if (ParseElement(block.Content?.ToString()) is not IPublishedElement content) 108 | { 109 | return null; 110 | } 111 | 112 | var contentType = content.GetType(); 113 | var genericViewModelFactoryType = typeof(IContentBlockViewModelFactory<>).MakeGenericType(new[] { contentType }); 114 | 115 | if (_serviceProvider.GetService(genericViewModelFactoryType) is not IContentBlockViewModelFactory viewModelFactory) 116 | { 117 | return null; 118 | } 119 | 120 | return viewModelFactory.Create(content, block.Id, block.DefinitionId, block.LayoutId); 121 | } 122 | 123 | IPublishedElement? ParseElement(string? blockContent) 124 | => _nestedContentSingleValueConverter.ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, blockContent, preview) as IPublishedElement; 125 | } 126 | 127 | public override bool? IsValue(object? value, PropertyValueLevel level) 128 | { 129 | if (level != PropertyValueLevel.Object) 130 | { 131 | // We only want to check at the Object level to prevent duplicate parsing logic 132 | return null; 133 | } 134 | 135 | if (value is not IContentBlocks model) 136 | { 137 | // Value must be invalid 138 | return false; 139 | } 140 | 141 | // Valid with at least 1 block 142 | return model.Header is not null || model.Blocks.Any(); 143 | } 144 | 145 | public override Type GetPropertyValueType(IPublishedPropertyType propertyType) 146 | => typeof(IContentBlocks); 147 | } 148 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ContentBlocksValueEditor.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 4 | using Perplex.ContentBlocks.Utils; 5 | using Umbraco.Cms.Core.Models; 6 | using Umbraco.Cms.Core.Models.Editors; 7 | using Umbraco.Cms.Core.PropertyEditors; 8 | using Umbraco.Cms.Core.Serialization; 9 | using Umbraco.Cms.Core.Services; 10 | using Umbraco.Cms.Core.Strings; 11 | 12 | namespace Perplex.ContentBlocks.PropertyEditor; 13 | 14 | public class ContentBlocksValueEditor : DataValueEditor, IDataValueReference 15 | { 16 | private readonly ContentBlocksModelValueDeserializer _deserializer; 17 | private readonly ContentBlockUtils _utils; 18 | 19 | private readonly IShortStringHelper _shortStringHelper; 20 | 21 | public ContentBlocksValueEditor( 22 | ContentBlocksModelValueDeserializer deserializer, 23 | ContentBlockUtils utils, 24 | ILocalizedTextService localizedTextService, 25 | IShortStringHelper shortStringHelper, 26 | IJsonSerializer jsonSerializer) : base(localizedTextService, shortStringHelper, jsonSerializer) 27 | { 28 | _deserializer = deserializer; 29 | _utils = utils; 30 | _shortStringHelper = shortStringHelper; 31 | } 32 | 33 | public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) 34 | { 35 | var json = editorValue.Value?.ToString(); 36 | var modelValue = _deserializer.Deserialize(json); 37 | if (modelValue == null) 38 | { 39 | return base.FromEditor(editorValue, currentValue); 40 | } 41 | 42 | if (modelValue.Header is ContentBlockModelValue header) 43 | { 44 | header.Content = FromEditor(header.Content, header.DefinitionId); 45 | 46 | foreach (var variant in header.Variants ?? Enumerable.Empty()) 47 | { 48 | variant.Content = FromEditor(variant.Content, header.DefinitionId); 49 | } 50 | } 51 | 52 | foreach (var block in modelValue.Blocks ?? Enumerable.Empty()) 53 | { 54 | block.Content = FromEditor(block.Content, block.DefinitionId); 55 | 56 | foreach (var variant in block.Variants ?? Enumerable.Empty()) 57 | { 58 | variant.Content = FromEditor(variant.Content, block.DefinitionId); 59 | } 60 | } 61 | 62 | return JsonConvert.SerializeObject(modelValue, Formatting.None); 63 | 64 | JArray? FromEditor(JArray? blockContent, Guid blockDefinitionId) 65 | { 66 | if (blockContent?.ToString() is string content && 67 | !string.IsNullOrWhiteSpace(content) && 68 | _utils.GetDataType(blockDefinitionId) is IDataType dataType && 69 | dataType.Editor?.GetValueEditor() is IDataValueEditor valueEditor) 70 | { 71 | var propertyData = new ContentPropertyData(content, dataType.Configuration); 72 | 73 | try 74 | { 75 | var ncJson = valueEditor.FromEditor(propertyData, null)?.ToString(); 76 | 77 | if (!string.IsNullOrWhiteSpace(ncJson)) 78 | { 79 | return JArray.Parse(ncJson); 80 | } 81 | } 82 | catch 83 | { 84 | return blockContent; 85 | } 86 | } 87 | 88 | // Fallback: return the original value 89 | return blockContent; 90 | } 91 | } 92 | 93 | public override object? ToEditor(IProperty property, string? culture = null, string? segment = null) 94 | 95 | { 96 | var json = property.GetValue(culture, segment)?.ToString(); 97 | var modelValue = _deserializer.Deserialize(json); 98 | if (modelValue == null) 99 | { 100 | return base.ToEditor(property, culture, segment); 101 | } 102 | 103 | JArray? ToEditor(JArray? blockContent, Guid blockDefinitionId) 104 | { 105 | if (blockContent?.ToString() is string content && 106 | !string.IsNullOrWhiteSpace(content) && 107 | _utils.GetDataType(blockDefinitionId) is IDataType dataType && 108 | dataType.Editor?.GetValueEditor() is IDataValueEditor valueEditor) 109 | { 110 | var ncPropType = new PropertyType(_shortStringHelper, dataType); 111 | if (culture != null) ncPropType.Variations |= ContentVariation.Culture; 112 | if (segment != null) ncPropType.Variations |= ContentVariation.Segment; 113 | 114 | var ncProperty = new Property(ncPropType); 115 | ncProperty.SetValue(content, culture, segment); 116 | 117 | try 118 | { 119 | if (valueEditor.ToEditor(ncProperty, culture, segment) is object ncValue) 120 | { 121 | return JArray.FromObject(ncValue); 122 | }; 123 | } 124 | catch 125 | { 126 | return blockContent; 127 | } 128 | } 129 | 130 | // Fallback: return the original value 131 | return blockContent; 132 | } 133 | 134 | if (modelValue.Header is ContentBlockModelValue header) 135 | { 136 | header.Content = ToEditor(header.Content, header.DefinitionId); 137 | 138 | foreach (var variant in header.Variants ?? Enumerable.Empty()) 139 | { 140 | variant.Content = ToEditor(variant.Content, header.DefinitionId); 141 | } 142 | } 143 | 144 | foreach (var block in modelValue.Blocks ?? Enumerable.Empty()) 145 | { 146 | block.Content = ToEditor(block.Content, block.DefinitionId); 147 | 148 | foreach (var variant in block.Variants ?? Enumerable.Empty()) 149 | { 150 | variant.Content = ToEditor(variant.Content, block.DefinitionId); 151 | } 152 | } 153 | 154 | return JObject.FromObject(modelValue); 155 | } 156 | 157 | public IEnumerable GetReferences(object? value) 158 | { 159 | var result = new List(); 160 | var json = value?.ToString(); 161 | 162 | var modelValue = _deserializer.Deserialize(json); 163 | if (modelValue is null) 164 | return result; 165 | 166 | if (modelValue.Header != null) 167 | { 168 | result.AddRange(GetReferencesByBlock(modelValue.Header)); 169 | } 170 | 171 | if (modelValue.Blocks?.Any() is true) 172 | { 173 | foreach (var block in modelValue.Blocks) 174 | { 175 | result.AddRange(GetReferencesByBlock(block)); 176 | } 177 | } 178 | 179 | IEnumerable GetReferencesByBlock(ContentBlockModelValue model) 180 | { 181 | if (_utils.GetDataType(model.DefinitionId) is IDataType dataType && dataType.Editor?.GetValueEditor() is IDataValueReference valueEditor) 182 | { 183 | var blockReferences = valueEditor.GetReferences(model.Content?.ToString()); 184 | var variantReferences = model.Variants?.SelectMany(v => valueEditor.GetReferences(v.Content?.ToString())) ?? Enumerable.Empty(); 185 | return blockReferences.Concat(variantReferences); 186 | } 187 | else 188 | { 189 | return Enumerable.Empty(); 190 | } 191 | } 192 | 193 | return result; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlockInterValue.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 4 | 5 | public class ContentBlockInterValue 6 | { 7 | public Guid Id { get; set; } 8 | public Guid DefinitionId { get; set; } 9 | public Guid LayoutId { get; set; } 10 | public JArray? Content { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlockModelValue.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 5 | 6 | public class ContentBlockModelValue 7 | { 8 | [JsonProperty("id")] 9 | public Guid Id { get; set; } 10 | 11 | [JsonProperty("definitionId")] 12 | public Guid DefinitionId { get; set; } 13 | 14 | [JsonProperty("layoutId")] 15 | public Guid LayoutId { get; set; } 16 | 17 | /// 18 | /// Indien dit blok uit een preset komt zal dit een waarde hebben 19 | /// en wijzen naar de betreffende IContentBlockPreset 20 | /// 21 | [JsonProperty("presetId")] 22 | public Guid? PresetId { get; set; } 23 | 24 | [JsonProperty("isDisabled")] 25 | public bool IsDisabled { get; set; } 26 | 27 | /// 28 | /// JSON NestedContent 29 | /// 30 | [JsonProperty("content")] 31 | public JArray? Content { get; set; } 32 | 33 | [JsonProperty("variants")] 34 | public List? Variants { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlockVariantModelValue.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 5 | 6 | public class ContentBlockVariantModelValue 7 | { 8 | [JsonProperty("id")] 9 | public Guid Id { get; set; } 10 | 11 | [JsonProperty("alias")] 12 | public string Alias { get; set; } = ""; 13 | 14 | /// 15 | /// JSON NestedContent 16 | /// 17 | [JsonProperty("content")] 18 | public JArray? Content { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlocksInterValue.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 2 | 3 | public class ContentBlocksInterValue 4 | { 5 | public ContentBlockInterValue? Header { get; set; } 6 | 7 | public IEnumerable Blocks { get; set; } 8 | = Array.Empty(); 9 | } 10 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlocksModelValue.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 4 | 5 | public class ContentBlocksModelValue 6 | { 7 | [JsonProperty("version")] 8 | public int Version { get; set; } 9 | 10 | [JsonProperty("header")] 11 | public ContentBlockModelValue? Header { get; set; } 12 | 13 | [JsonProperty("blocks")] 14 | public List? Blocks { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlocksModelValueComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | using Umbraco.Cms.Core.Notifications; 5 | 6 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 7 | 8 | public class ContentBlocksModelValueComposer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | builder.Services.AddSingleton(); 13 | builder.AddNotificationHandler(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlocksModelValueCopyingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Umbraco.Cms.Core.Events; 5 | using Umbraco.Cms.Core.Models; 6 | using Umbraco.Cms.Core.Notifications; 7 | 8 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 9 | 10 | public class ContentBlocksModelValueCopyingHandler : INotificationHandler 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public ContentBlocksModelValueCopyingHandler(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public void Handle(ContentCopyingNotification notification) 20 | => UpdateContentBlocksKeys(notification.Copy); 21 | 22 | private void UpdateContentBlocksKeys(IContent entity) 23 | { 24 | try 25 | { 26 | var properties = entity.Properties.Where(p => p.PropertyType.PropertyEditorAlias == Constants.PropertyEditor.Alias); 27 | foreach (var prop in properties) 28 | { 29 | // Update all property values -- i.e. all variants 30 | foreach (var propValue in prop.Values) 31 | { 32 | if (propValue is null) continue; 33 | 34 | string? culture = propValue.Culture; 35 | string? segment = propValue.Segment; 36 | 37 | string? value = propValue.EditedValue as string; 38 | if (string.IsNullOrEmpty(value)) 39 | { 40 | continue; 41 | } 42 | 43 | var contentBlocks = JsonConvert.DeserializeObject(value); 44 | 45 | var header = contentBlocks?.Value("header"); 46 | if (header is not null) 47 | { 48 | UpdateContentBlockKeys(header); 49 | } 50 | 51 | if (contentBlocks?.Value("blocks") is JArray blocks) 52 | { 53 | foreach (JObject block in blocks) 54 | { 55 | UpdateContentBlockKeys(block); 56 | } 57 | } 58 | 59 | prop.SetValue(JsonConvert.SerializeObject(contentBlocks), culture: culture, segment: segment); 60 | } 61 | } 62 | } 63 | catch (Exception ex) 64 | { 65 | _logger.LogError(ex, "Failed to update ContentBlock keys and IDs while copying a node."); 66 | } 67 | } 68 | 69 | private void UpdateContentBlockKeys(JObject block) 70 | { 71 | block["id"] = Guid.NewGuid(); 72 | 73 | if (block.Value("content") is JArray nestedContentItems) 74 | { 75 | foreach (var nestedContentItem in nestedContentItems) 76 | { 77 | UpdateNestedContentKey(nestedContentItem as JObject); 78 | } 79 | } 80 | 81 | var variants = block.Value("variants"); 82 | if (variants != null) 83 | { 84 | foreach (JObject variant in variants) 85 | { 86 | UpdateContentBlockKeys(variant); 87 | } 88 | } 89 | } 90 | 91 | private void UpdateNestedContentKey(JObject? nestedContent) 92 | { 93 | if (nestedContent is null || nestedContent["key"] == null) 94 | { 95 | return; 96 | } 97 | 98 | nestedContent["key"] = Guid.NewGuid(); 99 | 100 | // Also update any nested Nested Content items inside this nestedContent 101 | foreach (var property in nestedContent.Properties()) 102 | { 103 | // NestedContent stores nested NestedContent as strings rather than arrays for some reason, 104 | // i.e. if one of the properties inside this object is also 105 | // a NestedContent its value will be a string that looks like this: 106 | // "[{ \"key\": \" ... \" }]" instead of a real array like [{ "key": "..." }] 107 | // This means we have to actually parse each value to check if it's a JArray or not. 108 | if ( 109 | property.Value is JValue value && 110 | value.Type == JTokenType.String && 111 | value.ToString() is string rawValue && 112 | rawValue.TrimStart().StartsWith("[") 113 | ) 114 | { 115 | try 116 | { 117 | var array = JArray.Parse(value.ToString()); 118 | 119 | if (IsNestedContentValue(array)) 120 | { 121 | foreach (var child in array.Children()) 122 | { 123 | UpdateNestedContentKey(child); 124 | } 125 | 126 | nestedContent[property.Name] = JsonConvert.SerializeObject(array); 127 | } 128 | } 129 | catch (JsonReaderException) 130 | { 131 | // Not actually an Array -- just ignore 132 | } 133 | } 134 | } 135 | } 136 | 137 | private bool IsNestedContentValue(JArray value) 138 | { 139 | return value.First is JObject obj && obj["key"] != null && obj["ncContentTypeAlias"] != null; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/PropertyEditor/ModelValue/ContentBlocksModelValueDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Perplex.ContentBlocks.PropertyEditor.ModelValue; 4 | 5 | public class ContentBlocksModelValueDeserializer 6 | { 7 | /// 8 | /// Deserializes the given JSON to an instance of ContentBlocksModelValue 9 | /// 10 | /// JSON to deserialize 11 | /// 12 | public ContentBlocksModelValue? Deserialize(string? json) 13 | { 14 | if (string.IsNullOrWhiteSpace(json)) 15 | { 16 | return null; 17 | } 18 | 19 | try 20 | { 21 | var modelValue = JsonConvert.DeserializeObject(json); 22 | if (modelValue is null) return null; 23 | return MaybeTransformData(modelValue); 24 | } 25 | catch 26 | { 27 | return null; 28 | } 29 | } 30 | 31 | private static ContentBlocksModelValue MaybeTransformData(ContentBlocksModelValue modelValue) 32 | { 33 | if (modelValue.Version < 3) 34 | { 35 | // We added a Variants property in v3, for any older version we will ensure this property becomes an empty Array. 36 | if (modelValue.Header != null && modelValue.Header.Variants == null) 37 | { 38 | modelValue.Header.Variants = new List(); 39 | } 40 | 41 | if (modelValue.Blocks != null) 42 | { 43 | foreach (var block in modelValue.Blocks) 44 | { 45 | if (block.Variants == null) 46 | { 47 | block.Variants = new List(); 48 | } 49 | } 50 | } 51 | } 52 | 53 | return modelValue; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Providers/ContentBlockProvidersComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | 5 | namespace Perplex.ContentBlocks.Providers; 6 | 7 | public class ContentBlockProvidersComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddSingleton(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Providers/DocumentTypeAliasProvider.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models; 2 | using Umbraco.Cms.Core.Services; 3 | 4 | namespace Perplex.ContentBlocks.Providers; 5 | 6 | public class DocumentTypeAliasProvider : IDocumentTypeAliasProvider 7 | { 8 | private readonly IContentService _contentService; 9 | 10 | public DocumentTypeAliasProvider(IContentService contentService) 11 | { 12 | _contentService = contentService; 13 | } 14 | 15 | public string? GetDocumentTypeAlias(int pageId) 16 | { 17 | IContent? content = _contentService.GetById(pageId); 18 | if (content == null) 19 | { 20 | return null; 21 | } 22 | 23 | return content.ContentType?.Alias; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Providers/IDocumentTypeAliasProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Providers; 2 | 3 | public interface IDocumentTypeAliasProvider 4 | { 5 | string? GetDocumentTypeAlias(int pageId); 6 | } 7 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlockRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | using Perplex.ContentBlocks.Preview; 3 | 4 | namespace Perplex.ContentBlocks.Rendering; 5 | 6 | [Obsolete($"Use {nameof(ContentBlocksRenderer)} instead.")] 7 | public class ContentBlockRenderer : IContentBlockRenderer 8 | { 9 | private readonly bool _isPreview; 10 | private readonly IContentBlocksRenderer _renderer; 11 | 12 | public ContentBlockRenderer( 13 | IPreviewModeProvider previewModeProvider, 14 | IContentBlocksRenderer renderer) 15 | { 16 | _isPreview = previewModeProvider.IsPreviewMode; 17 | _renderer = renderer; 18 | } 19 | 20 | private static Task UnsupportedViewComponentRender(Type componentType, object? arguments) 21 | => throw new NotSupportedException($"ContentBlock definitions with view components are not supported by this renderer. Use {nameof(IContentBlocksRenderer)} instead."); 22 | 23 | public Task Render(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync) 24 | => _renderer.RenderAsync(contentBlocks, UnsupportedViewComponentRender, renderPartialViewAsync, _isPreview); 25 | 26 | public Task RenderBlocks(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync) 27 | => _renderer.RenderBlocksAsync(contentBlocks?.Blocks, UnsupportedViewComponentRender, renderPartialViewAsync, _isPreview); 28 | 29 | public Task RenderBlocks(IEnumerable? contentBlocks, RenderPartialViewAsync renderPartialViewAsync) 30 | => _renderer.RenderBlocksAsync(contentBlocks, UnsupportedViewComponentRender, renderPartialViewAsync, _isPreview); 31 | 32 | public Task RenderBlock(IContentBlockViewModel? contentBlockViewModel, RenderPartialViewAsync renderPartialViewAsync) 33 | => _renderer.RenderBlockAsync(contentBlockViewModel, UnsupportedViewComponentRender, renderPartialViewAsync, _isPreview); 34 | 35 | public Task RenderHeader(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync) 36 | => RenderBlock(contentBlocks?.Header, renderPartialViewAsync); 37 | } 38 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlockViewModel.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public class ContentBlockViewModel : IContentBlockViewModel 6 | { 7 | public Guid Id { get; } 8 | 9 | public Guid DefinitionId { get; } 10 | 11 | public Guid LayoutId { get; } 12 | 13 | public IPublishedElement Content { get; } 14 | 15 | public ContentBlockViewModel(IPublishedElement content, Guid definitionId, Guid layoutId) 16 | { 17 | Content = content; 18 | DefinitionId = definitionId; 19 | LayoutId = layoutId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlockViewModelFactory{T}.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public class ContentBlockViewModelFactory : IContentBlockViewModelFactory where TContent : class, IPublishedElement 6 | { 7 | public virtual IContentBlockViewModel Create(TContent content, Guid id, Guid definitionId, Guid layoutId) 8 | => new ContentBlockViewModel(content, id, definitionId, layoutId); 9 | 10 | IContentBlockViewModel? IContentBlockViewModelFactory.Create(IPublishedElement content, Guid id, Guid definitionId, Guid layoutId) 11 | { 12 | if (content is not TContent typedContent) return null; 13 | return Create(typedContent, id, definitionId, layoutId); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlockViewModel{T}.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public class ContentBlockViewModel : IContentBlockViewModel where TContent : IPublishedElement 6 | { 7 | public Guid Id { get; } 8 | 9 | public Guid DefinitionId { get; set; } 10 | 11 | public Guid LayoutId { get; set; } 12 | 13 | public TContent Content { get; set; } 14 | 15 | IPublishedElement IContentBlockViewModel.Content => Content; 16 | 17 | public ContentBlockViewModel(TContent content, Guid id, Guid definitionId, Guid layoutId) 18 | { 19 | Id = id; 20 | DefinitionId = definitionId; 21 | LayoutId = layoutId; 22 | Content = content; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlocks.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Rendering; 2 | 3 | public class ContentBlocks : IContentBlocks 4 | { 5 | public static readonly IContentBlocks Empty = new ContentBlocks(); 6 | 7 | public IContentBlockViewModel? Header { get; set; } 8 | 9 | public IEnumerable Blocks { get; set; } 10 | = Array.Empty(); 11 | } 12 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlocksRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Perplex.ContentBlocks.Definitions; 4 | 5 | namespace Perplex.ContentBlocks.Rendering; 6 | 7 | public class ContentBlocksRenderer : IContentBlocksRenderer 8 | { 9 | private readonly IContentBlockDefinitionRepository _definitionRepository; 10 | 11 | public ContentBlocksRenderer(IContentBlockDefinitionRepository definitionRepository) 12 | { 13 | _definitionRepository = definitionRepository; 14 | } 15 | 16 | /// 17 | public async Task RenderAsync(IContentBlocks? contentBlocks, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false) 18 | { 19 | if (contentBlocks is null) 20 | { 21 | return HtmlString.Empty; 22 | } 23 | 24 | var builder = new HtmlContentBuilder(); 25 | 26 | var blocksHtml = await Task.WhenAll( 27 | RenderBlockAsync(contentBlocks.Header, renderViewComponentAsync, renderPartialViewAsync, isBackOfficePreview), 28 | RenderBlocksAsync(contentBlocks.Blocks, renderViewComponentAsync, renderPartialViewAsync, isBackOfficePreview) 29 | ); 30 | 31 | foreach (var blockHtml in blocksHtml) 32 | { 33 | builder.AppendHtml(blockHtml); 34 | } 35 | 36 | return builder; 37 | } 38 | 39 | /// 40 | public async Task RenderBlockAsync(IContentBlockViewModel? block, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false) 41 | { 42 | if (block is null || 43 | _definitionRepository.GetById(block.DefinitionId) is not IContentBlockDefinition definition) 44 | { 45 | return HtmlString.Empty; 46 | } 47 | 48 | var blockHtml = await GetBlockHtml(block, definition, renderViewComponentAsync, renderPartialViewAsync); 49 | 50 | if (isBackOfficePreview) 51 | { 52 | // Preview mode: add block id for scroll synchronisation before the block html 53 | var blockIdAnchor = $""; 54 | return new HtmlContentBuilder() 55 | .AppendHtml(blockIdAnchor) 56 | .AppendHtml(blockHtml); 57 | } 58 | else 59 | { 60 | // No preview mode: block html only 61 | return blockHtml; 62 | } 63 | } 64 | 65 | /// 66 | public async Task RenderBlocksAsync(IEnumerable? blocks, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false) 67 | { 68 | if (blocks?.Any() != true) 69 | { 70 | return HtmlString.Empty; 71 | } 72 | 73 | var builder = new HtmlContentBuilder(); 74 | 75 | var renderTasks = blocks.Select(block => RenderBlockAsync(block, renderViewComponentAsync, renderPartialViewAsync, isBackOfficePreview)); 76 | var blocksHtml = await Task.WhenAll(renderTasks); 77 | 78 | foreach (var blockHtml in blocksHtml) 79 | { 80 | builder.AppendHtml(blockHtml); 81 | } 82 | 83 | return builder; 84 | } 85 | 86 | private static async Task GetBlockHtml(IContentBlockViewModel block, IContentBlockDefinition definition, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync) 87 | { 88 | if (definition is IContentBlockDefinition componentDefinition) 89 | { 90 | Type viewComponentType = componentDefinition.GetType().GenericTypeArguments[0]; 91 | return await renderViewComponentAsync(viewComponentType, block); 92 | } 93 | else 94 | { 95 | var viewPath = definition.Layouts.FirstOrDefault(l => l.Id == block.LayoutId)?.ViewPath; 96 | if (string.IsNullOrEmpty(viewPath)) 97 | { 98 | return HtmlString.Empty; 99 | } 100 | 101 | return await renderPartialViewAsync(viewPath, block); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlocksRenderingComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | using Umbraco.Cms.Core.Composing; 4 | using Umbraco.Cms.Core.DependencyInjection; 5 | 6 | namespace Perplex.ContentBlocks.Rendering; 7 | 8 | public class ContentBlocksRenderingComposer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | // Renderer 13 | #pragma warning disable CS0618 // For backwards compatibility 14 | builder.Services.AddScoped(); 15 | #pragma warning restore CS0618 // For backwards compatibility 16 | 17 | // New renderer that supports view components 18 | builder.Services.AddSingleton(); 19 | 20 | // General View Model factory 21 | builder.Services.AddSingleton( 22 | typeof(IContentBlockViewModelFactory<>), 23 | typeof(ContentBlockViewModelFactory<>)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/ContentBlocksTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | using Microsoft.AspNetCore.Mvc.ViewComponents; 4 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 5 | using Microsoft.AspNetCore.Razor.TagHelpers; 6 | using Perplex.ContentBlocks.Preview; 7 | using System.Reflection; 8 | 9 | namespace Perplex.ContentBlocks.Rendering; 10 | 11 | /// 12 | /// Tag helper to render ContentBlocks 13 | /// 14 | [HtmlTargetElement("perplex-content-blocks", TagStructure = TagStructure.WithoutEndTag)] 15 | public class ContentBlocksTagHelper : TagHelper 16 | { 17 | private readonly IViewComponentHelper _viewComponentHelper; 18 | private readonly IHtmlHelper _htmlHelper; 19 | private readonly IPreviewModeProvider _previewModeProvider; 20 | private readonly IContentBlocksRenderer _renderer; 21 | 22 | public ContentBlocksTagHelper( 23 | IViewComponentHelper viewComponentHelper, 24 | IHtmlHelper htmlHelper, 25 | IPreviewModeProvider previewModeProvider, 26 | IContentBlocksRenderer renderer) 27 | { 28 | _viewComponentHelper = viewComponentHelper; 29 | _htmlHelper = htmlHelper; 30 | _previewModeProvider = previewModeProvider; 31 | _renderer = renderer; 32 | } 33 | 34 | /// 35 | /// The content to render 36 | /// 37 | public IContentBlocks? Content { get; set; } 38 | 39 | /// 40 | /// A single ContentBlock to render 41 | /// 42 | public IContentBlockViewModel? Block { get; set; } 43 | 44 | /// 45 | /// Multiple ContentBlocks to render 46 | /// 47 | public IEnumerable? Blocks { get; set; } 48 | 49 | [HtmlAttributeNotBound] 50 | [ViewContext] 51 | public ViewContext? ViewContext { get; set; } 52 | 53 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 54 | { 55 | output.TagName = null; 56 | 57 | var blocks = GetBlocks().ToArray(); 58 | 59 | if (blocks.Length == 0 || ViewContext is null) 60 | { 61 | return; 62 | } 63 | 64 | EnsureViewContext(_viewComponentHelper, ViewContext); 65 | EnsureViewContext(_htmlHelper, ViewContext); 66 | 67 | var html = await _renderer.RenderBlocksAsync( 68 | blocks, 69 | _viewComponentHelper.InvokeAsync, 70 | _htmlHelper.PartialAsync, 71 | _previewModeProvider.IsPreviewMode); 72 | 73 | output.Content.SetHtmlContent(html); 74 | } 75 | 76 | private IEnumerable GetBlocks() 77 | { 78 | if (Content?.Header is not null) 79 | { 80 | yield return Content.Header; 81 | } 82 | 83 | if (Content?.Blocks is not null) 84 | { 85 | foreach (var block in Content.Blocks) 86 | { 87 | yield return block; 88 | } 89 | } 90 | 91 | if (Block is not null) 92 | { 93 | yield return Block; 94 | } 95 | 96 | if (Blocks is not null) 97 | { 98 | foreach (var block in Blocks) 99 | { 100 | yield return block; 101 | } 102 | } 103 | } 104 | 105 | private static void EnsureViewContext(IViewComponentHelper viewComponentHelper, ViewContext viewContext) 106 | { 107 | if (viewComponentHelper is DefaultViewComponentHelper defaultViewComponentHelper) 108 | { 109 | // Default case 110 | defaultViewComponentHelper.Contextualize(viewContext); 111 | return; 112 | } 113 | 114 | // Some other IViewComponentHelper: attempt to call a Contextualize method 115 | TryCallContextualize(viewComponentHelper, viewContext); 116 | } 117 | 118 | private static void EnsureViewContext(IHtmlHelper htmlHelper, ViewContext viewContext) 119 | { 120 | if (htmlHelper is HtmlHelper defaultHtmlHelper) 121 | { 122 | // Default case 123 | defaultHtmlHelper.Contextualize(viewContext); 124 | return; 125 | } 126 | 127 | // Some other IHtmlHelper: attempt to call a Contextualize method 128 | TryCallContextualize(htmlHelper, viewContext); 129 | } 130 | 131 | private static void TryCallContextualize(object instance, ViewContext viewContext) 132 | { 133 | Type[] parameterTypes = new[] { typeof(ViewContext) }; 134 | MethodInfo? methodInfo = instance.GetType().GetMethod("Contextualize", parameterTypes); 135 | methodInfo?.Invoke(instance, new[] { viewContext }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/Delegates.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | /// 6 | /// Renders a partial view with the given model 7 | /// 8 | /// Name of the partial view 9 | /// Model to pass to the partial view 10 | /// The rendered partial view 11 | public delegate Task RenderPartialViewAsync(string partialViewName, object model); 12 | 13 | /// 14 | /// Renders a view component with the given arguments 15 | /// 16 | /// The of the view component 17 | /// Arguments to pass to the view component 18 | /// The rendered view component 19 | public delegate Task RenderViewComponentAsync(Type componentType, object? arguments); 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/HtmlHelperExtensions.Rendering.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | 4 | namespace Perplex.ContentBlocks.Rendering; 5 | 6 | [Obsolete("Use tag helper instead.")] 7 | public static partial class HtmlHelperExtensions 8 | { 9 | /// 10 | /// Renders all Content Blocks. 11 | /// 12 | /// HtmlHelper 13 | /// Content Blocks to render 14 | /// Content Blocks renderer 15 | /// 16 | [Obsolete("Use tag helper instead.")] 17 | public static async Task RenderContentBlocks(this IHtmlHelper html, IContentBlocks? contentBlocks, IContentBlockRenderer renderer) 18 | => await renderer.Render(contentBlocks, html.PartialAsync); 19 | 20 | /// 21 | /// Renders a single Content Block 22 | /// 23 | /// HtmlHelper 24 | /// Content Block to render 25 | /// Content Blocks renderer 26 | /// 27 | [Obsolete("Use tag helper instead.")] 28 | public static async Task RenderContentBlock(this IHtmlHelper html, IContentBlockViewModel contentBlock, IContentBlockRenderer renderer) 29 | => await renderer.RenderBlock(contentBlock, html.PartialAsync); 30 | 31 | /// 32 | /// Renders multiple Content Blocks 33 | /// 34 | /// HtmlHelper 35 | /// Content Blocks to render 36 | /// Content Blocks renderer 37 | /// 38 | [Obsolete("Use tag helper instead.")] 39 | public static async Task RenderContentBlocks(this IHtmlHelper html, IEnumerable contentBlocks, IContentBlockRenderer renderer) 40 | => await renderer.RenderBlocks(contentBlocks, html.PartialAsync); 41 | } 42 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlockRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | [Obsolete($"Use {nameof(IContentBlocksRenderer)} instead which supports both partial views and view components")] 6 | public interface IContentBlockRenderer 7 | { 8 | Task Render(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync); 9 | 10 | Task RenderHeader(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync); 11 | 12 | Task RenderBlocks(IContentBlocks? contentBlocks, RenderPartialViewAsync renderPartialViewAsync); 13 | 14 | Task RenderBlock(IContentBlockViewModel? contentBlockViewModel, RenderPartialViewAsync renderPartialViewAsync); 15 | 16 | Task RenderBlocks(IEnumerable? contentBlocks, RenderPartialViewAsync renderPartialViewAsync); 17 | } 18 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlockViewModel.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public interface IContentBlockViewModel 6 | { 7 | Guid Id { get; } 8 | 9 | Guid DefinitionId { get; } 10 | 11 | Guid LayoutId { get; } 12 | 13 | IPublishedElement Content { get; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlockViewModelFactory.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public interface IContentBlockViewModelFactory 6 | { 7 | IContentBlockViewModel? Create(IPublishedElement content, Guid id, Guid definitionId, Guid layoutId); 8 | } 9 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlockViewModelFactory{T}.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public interface IContentBlockViewModelFactory : IContentBlockViewModelFactory where TContent : IPublishedElement 6 | { 7 | IContentBlockViewModel? Create(TContent content, Guid id, Guid definitionId, Guid layoutId); 8 | } 9 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlockViewModel{T}.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.PublishedContent; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | public interface IContentBlockViewModel : IContentBlockViewModel where TContent : IPublishedElement 6 | { 7 | new TContent Content { get; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlocks.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Rendering; 2 | 3 | public interface IContentBlocks 4 | { 5 | IContentBlockViewModel? Header { get; } 6 | IEnumerable Blocks { get; } 7 | } 8 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Rendering/IContentBlocksRenderer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | 3 | namespace Perplex.ContentBlocks.Rendering; 4 | 5 | /// 6 | /// Renders content blocks 7 | /// 8 | public interface IContentBlocksRenderer 9 | { 10 | /// 11 | /// Renders all content blocks 12 | /// 13 | /// The content blocks to render 14 | /// View component render method 15 | /// Partial view render method 16 | /// Indicates if the content blocks are rendered for the preview window in the backoffice 17 | /// The rendered content blocks 18 | Task RenderAsync(IContentBlocks? contentBlocks, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false); 19 | 20 | /// 21 | /// Renders a single content block 22 | /// 23 | /// The content block to render 24 | /// View component render method 25 | /// Partial view render method 26 | /// Indicates if the content blocks are rendered for the preview window in the backoffice 27 | /// The rendered content block 28 | Task RenderBlockAsync(IContentBlockViewModel? block, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false); 29 | 30 | /// 31 | /// Renders multiple content blocks 32 | /// 33 | /// The content blocks to render 34 | /// View component render method 35 | /// Partial view render method 36 | /// Indicates if the content blocks are rendered for the preview window in the backoffice 37 | /// The rendered content blocks 38 | Task RenderBlocksAsync(IEnumerable? blocks, RenderViewComponentAsync renderViewComponentAsync, RenderPartialViewAsync renderPartialViewAsync, bool isBackOfficePreview = false); 39 | } 40 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Utils/ContentBlockUtils.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.Definitions; 2 | 3 | using Umbraco.Cms.Core.Models; 4 | using Umbraco.Cms.Core.Services; 5 | using static Umbraco.Cms.Core.Constants.PropertyEditors; 6 | 7 | namespace Perplex.ContentBlocks.Utils; 8 | 9 | /// 10 | /// General ContentBlocks utility functions 11 | /// 12 | public class ContentBlockUtils 13 | { 14 | private readonly IDataTypeService _dataTypeService; 15 | private readonly Lazy _definitionRepository; 16 | 17 | public ContentBlockUtils(IDataTypeService dataTypeService, Lazy definitionRepository) 18 | { 19 | _dataTypeService = dataTypeService; 20 | _definitionRepository = definitionRepository; 21 | } 22 | 23 | /// 24 | /// Returns the dataType associated with the ContentBlock with the given definitionId. 25 | /// 26 | /// Id of the ContentBlock definition 27 | /// 28 | public IDataType? GetDataType(Guid definitionId) 29 | { 30 | var definition = _definitionRepository.Value.GetById(definitionId); 31 | if (definition is null) 32 | { 33 | return null; 34 | } 35 | 36 | return GetDataType(definition); 37 | } 38 | 39 | /// 40 | /// Returns the dataType associated with the given ContentBlock definition 41 | /// 42 | /// ContentBlock definition 43 | /// 44 | public IDataType? GetDataType(IContentBlockDefinition definition) 45 | { 46 | if (definition == null) 47 | { 48 | throw new ArgumentNullException(nameof(definition)); 49 | } 50 | 51 | IDataType? dataType = null; 52 | 53 | #pragma warning disable CS0618 // DataTypeId will still be used until removed in a next major 54 | if (definition.DataTypeId is int dataTypeId) 55 | { 56 | dataType = _dataTypeService.GetDataType(dataTypeId); 57 | } 58 | #pragma warning restore CS0618 // DataTypeId will still be used until removed in a next major 59 | else if (definition.DataTypeKey is Guid dataTypeKey) 60 | { 61 | dataType = _dataTypeService.GetDataType(dataTypeKey); 62 | } 63 | 64 | if (dataType == null) 65 | { 66 | return null; 67 | } 68 | 69 | if (dataType.EditorAlias != Aliases.NestedContent) 70 | { 71 | throw new InvalidOperationException($"DataType should be Nested Content, but was '{dataType.EditorAlias}'"); 72 | } 73 | 74 | return dataType; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Utils/ContentBlockUtilsComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | 5 | namespace Perplex.ContentBlocks.Utils; 6 | 7 | public class ContentBlockUtilsComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddSingleton(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Utils/Cookies/CookiesComposer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Umbraco.Cms.Core.Composing; 3 | using Umbraco.Cms.Core.DependencyInjection; 4 | 5 | namespace Perplex.ContentBlocks.Utils.Cookies; 6 | 7 | public class CookiesComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddScoped(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Utils/Cookies/HttpCookiesAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Perplex.ContentBlocks.Utils.Cookies; 4 | 5 | public class HttpCookiesAccessor : IHttpCookiesAccessor 6 | { 7 | public IDictionary Cookies { get; } 8 | 9 | public HttpCookiesAccessor(IHttpContextAccessor httpContextAccessor) 10 | { 11 | Cookies = new Dictionary(); 12 | 13 | if (httpContextAccessor.HttpContext is HttpContext httpCtx && 14 | httpCtx.Request.Cookies is IRequestCookieCollection cookies) 15 | { 16 | foreach (var kv in cookies) 17 | { 18 | Cookies[kv.Key] = kv.Value; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Utils/Cookies/IHttpCookiesAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.Utils.Cookies; 2 | 3 | public interface IHttpCookiesAccessor 4 | { 5 | IDictionary Cookies { get; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Variants/ContentBlockDefaultVariantSelector.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 2 | using Umbraco.Cms.Core.Models.PublishedContent; 3 | 4 | namespace Perplex.ContentBlocks.Variants; 5 | 6 | /// 7 | /// Variant selector that never selects a variant but will select the default block content. 8 | /// 9 | public class ContentBlockDefaultVariantSelector : IContentBlockVariantSelector 10 | { 11 | public ContentBlockVariantModelValue? SelectVariant(ContentBlockModelValue block, IPublishedElement content, bool preview) 12 | => null; 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Variants/ContentBlocksVariantsComposer.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Composing; 2 | using Umbraco.Cms.Core.DependencyInjection; 3 | using Umbraco.Extensions; 4 | 5 | namespace Perplex.ContentBlocks.Variants; 6 | 7 | public class ContentBlocksVariantsComposer : IComposer 8 | { 9 | public void Compose(IUmbracoBuilder builder) 10 | { 11 | builder.Services.AddUnique(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.Core/Variants/IContentBlockVariantSelector.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor.ModelValue; 2 | using Umbraco.Cms.Core.Models.PublishedContent; 3 | 4 | namespace Perplex.ContentBlocks.Variants; 5 | 6 | public interface IContentBlockVariantSelector 7 | { 8 | /// 9 | /// Select a variant to render for the given block. 10 | /// To render the default block content; return null. 11 | /// 12 | /// The block to select a variant for 13 | /// The content containing the block 14 | /// Indicates if this block is rendered in preview mode 15 | /// A variant to render for this block; or null if the default content should be rendered. 16 | ContentBlockVariantModelValue? SelectVariant(ContentBlockModelValue block, IPublishedElement content, bool preview); 17 | } 18 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/ApiContentBlockViewModel.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.DeliveryApi; 2 | 3 | namespace Perplex.ContentBlocks.DeliveryApi; 4 | 5 | public class ApiContentBlockViewModel : IApiContentBlockViewModel 6 | { 7 | public Guid Id { get; init; } 8 | 9 | public Guid DefinitionId { get; init; } 10 | 11 | public Guid LayoutId { get; init; } 12 | 13 | public IApiElement? Content { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/ApiContentBlocks.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.DeliveryApi; 2 | public class ApiContentBlocks 3 | { 4 | public static readonly ApiContentBlocks Empty = new(); 5 | 6 | public IApiContentBlockViewModel? Header { get; init; } 7 | public IEnumerable Blocks { get; init; } = Array.Empty(); 8 | } 9 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/Composer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Perplex.ContentBlocks.PropertyEditor; 4 | using Umbraco.Cms.Core.Composing; 5 | using Umbraco.Cms.Core.DependencyInjection; 6 | 7 | namespace Perplex.ContentBlocks.DeliveryApi; 8 | public class Composer : IComposer 9 | { 10 | public void Compose(IUmbracoBuilder builder) 11 | { 12 | builder.PropertyValueConverters().Remove(); 13 | 14 | // We use the original value converter in the Delivery API value converter so it needs to be registered. 15 | builder.Services 16 | .RemoveAll() 17 | .AddSingleton(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/ContentBlocksApiValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Perplex.ContentBlocks.PropertyEditor; 2 | using Perplex.ContentBlocks.Rendering; 3 | using Umbraco.Cms.Core.DeliveryApi; 4 | using Umbraco.Cms.Core.Models.PublishedContent; 5 | using Umbraco.Cms.Core.PropertyEditors; 6 | using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; 7 | 8 | namespace Perplex.ContentBlocks.DeliveryApi; 9 | 10 | public class ContentBlocksApiValueConverter : IDeliveryApiPropertyValueConverter 11 | { 12 | private readonly ContentBlocksValueConverter _contentBlocksValueConverter; 13 | private readonly IApiElementBuilder _apiElementBuilder; 14 | 15 | public ContentBlocksApiValueConverter( 16 | ContentBlocksValueConverter contentBlocksValueConverter, 17 | IApiElementBuilder apiElementBuilder) 18 | { 19 | _contentBlocksValueConverter = contentBlocksValueConverter; 20 | _apiElementBuilder = apiElementBuilder; 21 | } 22 | 23 | public bool IsConverter(IPublishedPropertyType propertyType) 24 | => propertyType.EditorAlias == Constants.PropertyEditor.Alias; 25 | 26 | public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) 27 | => GetPropertyCacheLevel(propertyType); 28 | 29 | public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) 30 | => typeof(IApiContentBlocks); 31 | 32 | public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) 33 | { 34 | var modelValue = _contentBlocksValueConverter.ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview); 35 | if (modelValue is not IContentBlocks contentBlocks) 36 | { 37 | return null; 38 | } 39 | 40 | return new ApiContentBlocks 41 | { 42 | Header = Map(contentBlocks.Header), 43 | Blocks = contentBlocks.Blocks.Select(Map).OfType().ToArray(), 44 | }; 45 | 46 | ApiContentBlockViewModel? Map(IContentBlockViewModel? vm) 47 | { 48 | if (vm is null) 49 | { 50 | return null; 51 | } 52 | 53 | return new ApiContentBlockViewModel 54 | { 55 | Id = vm.Id, 56 | DefinitionId = vm.DefinitionId, 57 | LayoutId = vm.LayoutId, 58 | Content = _apiElementBuilder.Build(vm.Content), 59 | }; 60 | } 61 | } 62 | 63 | #region Forwarded to ContentBlocksValueConverter 64 | public object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => _contentBlocksValueConverter.ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview); 65 | public object? ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => _contentBlocksValueConverter.ConvertIntermediateToXPath(owner, propertyType, referenceCacheLevel, inter, preview); 66 | public object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => _contentBlocksValueConverter.ConvertSourceToIntermediate(owner, propertyType, source, preview); 67 | public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => _contentBlocksValueConverter.GetPropertyCacheLevel(propertyType); 68 | public Type GetPropertyValueType(IPublishedPropertyType propertyType) => _contentBlocksValueConverter.GetPropertyValueType(propertyType); 69 | public bool? IsValue(object? value, PropertyValueLevel level) => _contentBlocksValueConverter.IsValue(value, level); 70 | #endregion 71 | } 72 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/IApiContentBlockViewModel.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Models.DeliveryApi; 2 | 3 | namespace Perplex.ContentBlocks.DeliveryApi; 4 | 5 | public interface IApiContentBlockViewModel 6 | { 7 | Guid Id { get; } 8 | 9 | Guid DefinitionId { get; } 10 | 11 | Guid LayoutId { get; } 12 | 13 | IApiElement? Content { get; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/IApiContentBlocks.cs: -------------------------------------------------------------------------------- 1 | namespace Perplex.ContentBlocks.DeliveryApi; 2 | public interface IApiContentBlocks 3 | { 4 | IApiContentBlockViewModel Header { get; } 5 | IEnumerable Blocks { get; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.DeliveryApi/Perplex.ContentBlocks.DeliveryApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | True 6 | False 7 | $(NoWarn);NU1902 8 | 9 | 10 | 11 | 1.0.0-rc.1 12 | Perplex.ContentBlocks.DeliveryApi 13 | Adds support for the Content Delivery API in Umbraco 12+ 14 | umbraco 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/ManifestFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Umbraco.Cms.Core.Manifest; 3 | 4 | namespace Perplex.ContentBlocks.StaticAssets; 5 | 6 | internal class ManifestFilter : IManifestFilter 7 | { 8 | void IManifestFilter.Filter(List manifests) 9 | { 10 | var assembly = typeof(PackageManifest).Assembly; 11 | var versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); 12 | 13 | manifests.Add(new PackageManifest 14 | { 15 | PackageName = "Perplex.ContentBlocks", 16 | Version = versionInfo.ProductVersion ?? "1.0.0", 17 | AllowPackageTelemetry = false, 18 | BundleOptions = BundleOptions.Default, 19 | Scripts = new[] 20 | { 21 | #if !RELEASE 22 | 23 | // Note: .requires.js should be loaded first 24 | "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.requires.js", 25 | 26 | "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.api.js", 27 | "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.controller.js", 28 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-block.component.js", 29 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.add-block.js", 30 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.custom-component.js", 31 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.custom-components.js", 32 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.icon.js", 33 | "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.nested-content-patch.js", 34 | "/App_Plugins/Perplex.ContentBlocks/configuration/perplex.content-blocks.configuration.structure.js", 35 | "/App_Plugins/Perplex.ContentBlocks/lib/angular-slick.js", 36 | "/App_Plugins/Perplex.ContentBlocks/lib/slick.min.js", 37 | "/App_Plugins/Perplex.ContentBlocks/utils/perplex.content-blocks.copy-paste-service.js", 38 | "/App_Plugins/Perplex.ContentBlocks/utils/perplex.content-blocks.utils.js", 39 | "/App_Plugins/Perplex.ContentBlocks/utils/portal/perplex.content-blocks-portal.js", 40 | "/App_Plugins/Perplex.ContentBlocks/utils/property/perplex.content-blocks.property-scaffold-cache.js", 41 | "/App_Plugins/Perplex.ContentBlocks/utils/property/perplex.content-blocks.property.js", 42 | "/App_Plugins/Perplex.ContentBlocks/utils/tab-focus/perplex.content-blocks.tab-focus-once.directive.js", 43 | "/App_Plugins/Perplex.ContentBlocks/utils/tab-focus/perplex.content-blocks.tab-focus.service.js", 44 | 45 | #elif RELEASE 46 | // In Release mode all JS is bundled into 1 file 47 | "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.js", 48 | #endif 49 | }, 50 | 51 | Stylesheets = new[] 52 | { 53 | "/App_Plugins/Perplex.ContentBlocks/perplex.content-blocks.css", 54 | }, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/ManifestFilterComposer.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Cms.Core.Composing; 2 | using Umbraco.Cms.Core.DependencyInjection; 3 | 4 | namespace Perplex.ContentBlocks.StaticAssets; 5 | 6 | public class ManifestFilterComposer : IComposer 7 | { 8 | public void Compose(IUmbracoBuilder builder) 9 | => builder.ManifestFilters().Append(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/Perplex.ContentBlocks.StaticAssets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Perplex.ContentBlocks.StaticAssets 5 | umbraco 6 | Perplex.ContentBlocks Static Assets 7 | / 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/build/Perplex.ContentBlocks.StaticAssets.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | PerplexContentBlocks_StaticAssets/IncludeAppPlugins;$(ResolveStaticWebAssetsInputsDependsOn) 5 | 6 | 7 | 8 | <_AppPluginsPrefix Condition="'$(Configuration)' == 'Release'">dist\ 9 | <_AppPluginsPrefix Condition="'$(Configuration)' != 'Release'">src\ 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <_AppPluginsFiles Include="$(_AppPluginsPrefix)App_Plugins\Perplex.ContentBlocks\**"> 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.add-block.html: -------------------------------------------------------------------------------- 1 | 
3 |
4 | 12 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.add-block.js: -------------------------------------------------------------------------------- 1 | angular.module("perplexContentBlocks").component("perplexContentBlocksAddBlock", { 2 | templateUrl: "/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.add-block.html", 3 | 4 | bindings: { 5 | addBlock: "&", 6 | paste: "&", 7 | canPaste: "<", 8 | show: "<", 9 | isHeader: "<", 10 | noPaddingTop: "<", 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.custom-component.js: -------------------------------------------------------------------------------- 1 | angular.module("perplexContentBlocks").component("perplexContentBlocksCustomComponent", { 2 | bindings: { 3 | component: "<", 4 | data: "<", 5 | }, 6 | controller: [ 7 | "$scope", 8 | "$compile", 9 | "$element", 10 | function perplexContentBlocksCustomComponent($scope, $compile, $element) { 11 | this.$onInit = function () { 12 | if (this.component == null) { 13 | throw new Error("perplexContentBlocksCustomComponent: component binding is required but missing"); 14 | } 15 | 16 | this.renderCustomComponent(); 17 | } 18 | 19 | this.renderCustomComponent = function () { 20 | var params = this.data == null ? "" : " " + Object.keys(this.data).map(function (key) { 21 | return key + '="$ctrl.data[\'' + key + '\']"'; 22 | }.bind(this)).join(" "); 23 | 24 | var template = '<' + this.component + params + '>'; 25 | 26 | var compiled = $compile(template)($scope); 27 | 28 | $element.append(compiled); 29 | } 30 | }, 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.custom-components.js: -------------------------------------------------------------------------------- 1 | angular.module("perplexContentBlocks").constant("perplexContentBlocksCustomComponents", { 2 | block: { 3 | main: null, 4 | buttons: [], 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.icon.js: -------------------------------------------------------------------------------- 1 | angular.module("perplexContentBlocks").component("contentBlocksIcon", { 2 | template: 3 | '' + 4 | '' + 5 | ' ', 6 | bindings: { 7 | icon: "@", 8 | size: "@?", 9 | cssClass: "@?", 10 | }, 11 | controller: function () { 12 | this.$onInit = function () { 13 | if (this.size == null) { 14 | // Default to small 15 | this.size = "sm"; 16 | } 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/components/perplex.content-blocks.nested-content-patch.js: -------------------------------------------------------------------------------- 1 | angular.module("umbraco") 2 | .decorator("nestedContentPropertyEditorDirective", function ($delegate, versionHelper) { 3 | // All versions below 8.15.0 need this patch. 4 | var patchIsNeeded = versionHelper.versionCompare(Umbraco.Sys.ServerVariables.application.version, "8.15.0") < 0; 5 | 6 | if (patchIsNeeded && Array.isArray($delegate) && $delegate.length > 0) { 7 | var component = $delegate[0]; 8 | if (typeof component.template === "string") { 9 | component.template += ""; 10 | } 11 | } 12 | 13 | return $delegate; 14 | }); 15 | 16 | angular.module("perplexContentBlocks").component("contentBlocksNestedContentPatch", { 17 | controller: [function contentBlocksNestedContentPatchController() { 18 | var $ctrl = this; 19 | 20 | this.$onInit = function() { 21 | if(this.ncEditor != null && Array.isArray(this.ncEditor.scaffolds)) { 22 | var pushFn = this.ncEditor.scaffolds.push; 23 | this.ncEditor.scaffolds.push = function() { 24 | for(var i = 0; i < arguments.length; i++) { 25 | var scaffold = arguments[i]; 26 | ensureCultureData(scaffold, $ctrl.ncEditor); 27 | } 28 | 29 | pushFn.apply($ctrl.ncEditor.scaffolds, arguments); 30 | } 31 | } 32 | } 33 | 34 | // Code from PR 10562: 35 | // https://github.com/umbraco/Umbraco-CMS/pull/10562/files 36 | // Updated arrow functions to regular functions and passed in "vm" variable 37 | // since we are running in a different context. 38 | function ensureCultureData(content, vm) { 39 | if (!content || !vm.umbVariantContent || !vm.umbProperty) return; 40 | 41 | if (vm.umbVariantContent.editor.content.language) { 42 | // set the scaffolded content's language to the language of the current editor 43 | content.language = vm.umbVariantContent.editor.content.language; 44 | } 45 | // currently we only ever deal with invariant content for blocks so there's only one 46 | content.variants[0].tabs.forEach(function(tab) { 47 | tab.properties.forEach(function(prop) { 48 | // set the scaffolded property to the culture of the containing property 49 | prop.culture = vm.umbProperty.property.culture; 50 | }); 51 | }); 52 | } 53 | }], 54 | 55 | require: { 56 | ncEditor: "^nestedContentPropertyEditor", 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/Perplex.ContentBlocks.StaticAssets/src/App_Plugins/Perplex.ContentBlocks/configuration/perplex.content-blocks.configuration.structure-view.html: -------------------------------------------------------------------------------- 1 | 
3 |