From 9aeb3ed9c5d139637910219bbf6913fe5a2bcdf3 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Thu, 23 May 2024 09:12:38 +0100 Subject: [PATCH 1/7] Feature: Umbraco Flavored Markdown --- package.json | 1 + src/apps/backoffice/backoffice.element.ts | 1 + .../consume/context-consumer.test.ts | 2 +- .../formatting-api/formatting.controller.ts | 2 + .../localizeAndTransform.function.ts | 3 + .../block-grid-block.element.ts | 27 +++- .../ref-list-block/ref-list-block.element.ts | 25 +++- .../core/components/table/table.element.ts | 19 ++- .../core/extension-registry/models/index.ts | 3 + .../models/ufm-component.model.ts | 15 +++ .../core/localization/localize.element.ts | 6 +- .../property-dataset.element.test.ts | 4 +- .../property-layout.element.ts | 20 +-- .../pagination.manager.test.ts | 2 +- .../document-grid-collection-view.element.ts | 28 ++++- .../document-table-collection-view.element.ts | 1 + .../column/column-configuration.element.ts | 15 ++- .../ufm/components/ufm-render/index.ts | 2 + .../ufm-render/ufm-render.context.ts | 25 ++++ .../ufm-render/ufm-render.element.ts | 119 ++++++++++++++++++ src/packages/ufm/index.ts | 2 + src/packages/ufm/manifests.ts | 22 ++++ src/packages/ufm/plugins/marked-ufm.plugin.ts | 41 ++++++ .../ufm-components/document-name.component.ts | 12 ++ .../ufm-components/document-name.element.ts | 54 ++++++++ .../ufm-components/label-value.component.ts | 12 ++ .../ufm/ufm-components/label-value.element.ts | 44 +++++++ .../ufm/ufm-components/localize.component.ts | 10 ++ .../ufm/ufm-components/ufm-component-base.ts | 7 ++ src/packages/ufm/umbraco-package.ts | 8 ++ tsconfig.json | 1 + 31 files changed, 489 insertions(+), 44 deletions(-) create mode 100644 src/packages/core/extension-registry/models/ufm-component.model.ts create mode 100644 src/packages/ufm/components/ufm-render/index.ts create mode 100644 src/packages/ufm/components/ufm-render/ufm-render.context.ts create mode 100644 src/packages/ufm/components/ufm-render/ufm-render.element.ts create mode 100644 src/packages/ufm/index.ts create mode 100644 src/packages/ufm/manifests.ts create mode 100644 src/packages/ufm/plugins/marked-ufm.plugin.ts create mode 100644 src/packages/ufm/ufm-components/document-name.component.ts create mode 100644 src/packages/ufm/ufm-components/document-name.element.ts create mode 100644 src/packages/ufm/ufm-components/label-value.component.ts create mode 100644 src/packages/ufm/ufm-components/label-value.element.ts create mode 100644 src/packages/ufm/ufm-components/localize.component.ts create mode 100644 src/packages/ufm/ufm-components/ufm-component-base.ts create mode 100644 src/packages/ufm/umbraco-package.ts diff --git a/package.json b/package.json index f60c783688..249ee40aea 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "./themes": "./dist-cms/packages/core/themes/index.js", "./tiny-mce": "./dist-cms/packages/tiny-mce/index.js", "./tree": "./dist-cms/packages/core/tree/index.js", + "./ufm": "./dist-cms/packages/ufm/index.js", "./user-group": "./dist-cms/packages/user/user-group/index.js", "./user-permission": "./dist-cms/packages/user/user-permission/index.js", "./user": "./dist-cms/packages/user/user/index.js", diff --git a/src/apps/backoffice/backoffice.element.ts b/src/apps/backoffice/backoffice.element.ts index 781762cf9d..e8544ac96a 100644 --- a/src/apps/backoffice/backoffice.element.ts +++ b/src/apps/backoffice/backoffice.element.ts @@ -33,6 +33,7 @@ const CORE_PACKAGES = [ import('../../packages/tags/umbraco-package.js'), import('../../packages/templating/umbraco-package.js'), import('../../packages/tiny-mce/umbraco-package.js'), + import('../../packages/ufm/umbraco-package.js'), import('../../packages/umbraco-news/umbraco-package.js'), import('../../packages/user/umbraco-package.js'), import('../../packages/webhook/umbraco-package.js'), diff --git a/src/libs/context-api/consume/context-consumer.test.ts b/src/libs/context-api/consume/context-consumer.test.ts index 9baf9ea886..bd14391528 100644 --- a/src/libs/context-api/consume/context-consumer.test.ts +++ b/src/libs/context-api/consume/context-consumer.test.ts @@ -39,7 +39,7 @@ describe('UmbContextConsumer', () => { describe('events', () => { it('dispatches context request event when constructed', async () => { - const listener = oneEvent(window, UMB_CONTENT_REQUEST_EVENT_TYPE, false); + const listener = oneEvent(window, UMB_CONTENT_REQUEST_EVENT_TYPE); consumer.hostConnected(); diff --git a/src/libs/formatting-api/formatting.controller.ts b/src/libs/formatting-api/formatting.controller.ts index 1465cd3bbd..e3e2c7578e 100644 --- a/src/libs/formatting-api/formatting.controller.ts +++ b/src/libs/formatting-api/formatting.controller.ts @@ -16,12 +16,14 @@ UmbDomPurify.addHook('afterSanitizeAttributes', function (node) { /** * @description - Controller for formatting text. + * @deprecated - Use the `` component instead. This method will be removed in Umbraco 15. */ export class UmbFormattingController extends UmbControllerBase { #localize = new UmbLocalizationController(this._host); /** * A method to localize the string input then transform any markdown to santized HTML. + * @deprecated - Use the `` component instead. This method will be removed in Umbraco 15. */ public transform(input?: string): string { if (!input) return ''; diff --git a/src/libs/formatting-api/localizeAndTransform.function.ts b/src/libs/formatting-api/localizeAndTransform.function.ts index 21ff4e07cd..a066d53bb9 100644 --- a/src/libs/formatting-api/localizeAndTransform.function.ts +++ b/src/libs/formatting-api/localizeAndTransform.function.ts @@ -1,6 +1,9 @@ import { UmbFormattingController } from './formatting.controller.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +/** + * @deprecated - Use the `` component instead. This method will be removed in Umbraco 15. + */ export function localizeAndTransform(host: UmbControllerHost, input: string): string { return new UmbFormattingController(host).transform(input); } diff --git a/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts b/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts index 860783fca9..3069fc1050 100644 --- a/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts +++ b/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts @@ -1,8 +1,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../../context/block-grid-entry.context-token.js'; +import type { UmbBlockDataType, UmbBlockViewUrlsPropType } from '@umbraco-cms/backoffice/block'; + +import '@umbraco-cms/backoffice/ufm'; import '../block-grid-areas-container/index.js'; import '../ref-grid-block/index.js'; -import type { UmbBlockViewUrlsPropType } from '@umbraco-cms/backoffice/block'; /** * @element umb-block-grid-block @@ -16,8 +19,26 @@ export class UmbBlockGridBlockElement extends UmbLitElement { @property({ attribute: false }) urls?: UmbBlockViewUrlsPropType; + @state() + _content?: UmbBlockDataType; + + constructor() { + super(); + + this.consumeContext(UMB_BLOCK_GRID_ENTRY_CONTEXT, (context) => { + this.observe( + context.content, + (content) => { + this._content = content; + }, + 'observeContent', + ); + }); + } + override render() { - return html` + return html` + `; } diff --git a/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts b/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts index 5a03ff0691..2c167d839a 100644 --- a/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts +++ b/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts @@ -1,6 +1,9 @@ -import { UMB_BLOCK_ENTRY_CONTEXT } from '@umbraco-cms/backoffice/block'; import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_BLOCK_ENTRY_CONTEXT } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block'; + +import '@umbraco-cms/backoffice/ufm'; /** * @element umb-ref-list-block @@ -11,6 +14,9 @@ export class UmbRefListBlockElement extends UmbLitElement { @property({ type: String }) label?: string; + @state() + _content?: UmbBlockDataType; + @state() _workspaceEditPath?: string; @@ -19,6 +25,14 @@ export class UmbRefListBlockElement extends UmbLitElement { // UMB_BLOCK_LIST_ENTRY_CONTEXT this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => { + this.observe( + context.content, + (content) => { + this._content = content; + }, + 'observeContent', + ); + this.observe( context.workspaceEditContentPath, (workspaceEditPath) => { @@ -30,10 +44,11 @@ export class UmbRefListBlockElement extends UmbLitElement { } override render() { - return html``; + return html` + + + + `; } static override styles = [ diff --git a/src/packages/core/components/table/table.element.ts b/src/packages/core/components/table/table.element.ts index 711390d6b1..d400a230f9 100644 --- a/src/packages/core/components/table/table.element.ts +++ b/src/packages/core/components/table/table.element.ts @@ -1,14 +1,15 @@ +import type { UmbUfmRenderElement } from '../../../ufm/components/ufm-render/index.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, + customElement, html, - LitElement, ifDefined, - when, - customElement, property, - state, repeat, + state, + when, + LitElement, } from '@umbraco-cms/backoffice/external/lit'; // TODO: move to UI Library - entity actions should NOT be moved to UI Library but stay in an UmbTable element @@ -31,6 +32,7 @@ export interface UmbTableColumn { width?: string; allowSorting?: boolean; align?: 'left' | 'center' | 'right'; + labelTemplate?: string; } export interface UmbTableColumnLayoutElement extends HTMLElement { @@ -263,6 +265,15 @@ export class UmbTableElement extends LitElement { return element; } + if (column.labelTemplate) { + import('@umbraco-cms/backoffice/ufm'); + const element = document.createElement('umb-ufm-render') as UmbUfmRenderElement; + element.inline = true; + element.markdown = column.labelTemplate; + element.value = value; + return element; + } + return value; } diff --git a/src/packages/core/extension-registry/models/index.ts b/src/packages/core/extension-registry/models/index.ts index b0aa5d38f4..47e891bd60 100644 --- a/src/packages/core/extension-registry/models/index.ts +++ b/src/packages/core/extension-registry/models/index.ts @@ -45,6 +45,7 @@ import type { ManifestTheme } from './theme.model.js'; import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js'; import type { ManifestTree } from './tree.model.js'; import type { ManifestTreeItem } from './tree-item.model.js'; +import type { ManifestUfmComponent } from './ufm-component.model.js'; import type { ManifestUserProfileApp } from './user-profile-app.model.js'; import type { ManifestWorkspace, ManifestWorkspaceRoutableKind } from './workspace.model.js'; import type { ManifestWorkspaceAction, ManifestWorkspaceActionDefaultKind } from './workspace-action.model.js'; @@ -113,6 +114,7 @@ export type * from './theme.model.js'; export type * from './tinymce-plugin.model.js'; export type * from './tree-item.model.js'; export type * from './tree.model.js'; +export type * from './ufm-component.model.js'; export type * from './user-granular-permission.model.js'; export type * from './user-profile-app.model.js'; export type * from './workspace-action-menu-item.model.js'; @@ -204,6 +206,7 @@ export type ManifestTypes = | ManifestTree | ManifestTreeItem | ManifestTreeStore + | ManifestUfmComponent | ManifestUserProfileApp | ManifestWorkspaceActionMenuItem | ManifestWorkspaceActions diff --git a/src/packages/core/extension-registry/models/ufm-component.model.ts b/src/packages/core/extension-registry/models/ufm-component.model.ts new file mode 100644 index 0000000000..9c3a2afa14 --- /dev/null +++ b/src/packages/core/extension-registry/models/ufm-component.model.ts @@ -0,0 +1,15 @@ +import type { ManifestApi, UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { Tokens } from '@umbraco-cms/backoffice/external/marked'; + +export interface UmbUfmComponentApi extends UmbApi { + render(token: Tokens.Generic): string | undefined; +} + +export interface MetaUfmComponent { + marker: string; +} + +export interface ManifestUfmComponent extends ManifestApi { + type: 'ufmComponent'; + meta: MetaUfmComponent; +} diff --git a/src/packages/core/localization/localize.element.ts b/src/packages/core/localization/localize.element.ts index 98248571fe..945209f072 100644 --- a/src/packages/core/localization/localize.element.ts +++ b/src/packages/core/localization/localize.element.ts @@ -22,16 +22,14 @@ export class UmbLocalizeElement extends UmbLitElement { * @example args="[1,2,3]" * @type {any[] | undefined} */ - @property({ - type: Array, - }) + @property({ type: Array }) args?: unknown[]; /** * If true, the key will be rendered instead of the localized value if the key is not found. * @attr */ - @property() + @property({ type: Boolean }) debug = false; @state() diff --git a/src/packages/core/property/property-dataset/property-dataset.element.test.ts b/src/packages/core/property/property-dataset/property-dataset.element.test.ts index 09338ec71c..363788c23d 100644 --- a/src/packages/core/property/property-dataset/property-dataset.element.test.ts +++ b/src/packages/core/property/property-dataset/property-dataset.element.test.ts @@ -133,7 +133,7 @@ describe('UmbBasicVariantElement', () => { }); it('fires change event', async () => { - const listener = oneEvent(datasetElement, UmbChangeEvent.TYPE, false); + const listener = oneEvent(datasetElement, UmbChangeEvent.TYPE); expect(propertyEditor.alias).to.eq('testAlias'); propertyEditor.setValue('testValue3'); @@ -153,7 +153,7 @@ describe('UmbBasicVariantElement', () => { adapterPropertyEditor.alias = 'testAdapterAlias'; datasetElement.appendChild(adapterPropertyEditor); - const listener = oneEvent(datasetElement, UmbChangeEvent.TYPE, false); + const listener = oneEvent(datasetElement, UmbChangeEvent.TYPE); // The alias of the original property editor must be 'testAlias' for the adapter to set the value of it. expect(propertyEditor.alias).to.eq('testAlias'); diff --git a/src/packages/core/property/property-layout/property-layout.element.ts b/src/packages/core/property/property-layout/property-layout.element.ts index d010eb54ac..78ef7682db 100644 --- a/src/packages/core/property/property-layout/property-layout.element.ts +++ b/src/packages/core/property/property-layout/property-layout.element.ts @@ -1,8 +1,9 @@ -import { localizeAndTransform } from '@umbraco-cms/backoffice/formatting-api'; -import { css, customElement, html, property, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import '@umbraco-cms/backoffice/ufm'; + /** * @element umb-property-layout * @description - Element for displaying a property in an workspace. @@ -59,6 +60,7 @@ export class UmbPropertyLayoutElement extends UmbLitElement { public invalid?: boolean; override render() { + const ufmValue = { alias: this.alias, label: this.label, description: this.description }; // TODO: Only show alias on label if user has access to DocumentType within settings: return html`
@@ -68,7 +70,7 @@ export class UmbPropertyLayoutElement extends UmbLitElement { - ${unsafeHTML(localizeAndTransform(this, this.description))} +
@@ -126,18 +128,6 @@ export class UmbPropertyLayoutElement extends UmbLitElement { right: -30px; } - #description { - color: var(--uui-color-text-alt); - } - - #description * { - max-width: 100%; - } - - #description pre { - overflow: auto; - } - #editorColumn { margin-top: var(--uui-size-space-3); } diff --git a/src/packages/core/utils/pagination-manager/pagination.manager.test.ts b/src/packages/core/utils/pagination-manager/pagination.manager.test.ts index 3d2fe4f7a2..b18c0c2d30 100644 --- a/src/packages/core/utils/pagination-manager/pagination.manager.test.ts +++ b/src/packages/core/utils/pagination-manager/pagination.manager.test.ts @@ -141,7 +141,7 @@ describe('UmbPaginationManager', () => { }); it('dispatches a change event', async () => { - const listener = oneEvent(manager, UmbChangeEvent.TYPE, false); + const listener = oneEvent(manager, UmbChangeEvent.TYPE); manager.setCurrentPageNumber(2); const event = (await listener) as unknown as UmbChangeEvent; const target = event.target as UmbPaginationManager; diff --git a/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts b/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts index 9c03f937ef..f9be064582 100644 --- a/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts +++ b/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts @@ -1,15 +1,17 @@ -import { getPropertyValueByAlias } from '../index.js'; -import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; -import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } from '../../types.js'; -import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js'; import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { fromCamelCase } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import type { UmbDefaultCollectionContext, UmbCollectionColumnConfiguration } from '@umbraco-cms/backoffice/collection'; import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js'; +import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } from '../../types.js'; +import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; +import { getPropertyValueByAlias } from '../index.js'; + +import '@umbraco-cms/backoffice/ufm'; @customElement('umb-document-grid-collection-view') export class UmbDocumentGridCollectionViewElement extends UmbLitElement { @@ -170,7 +172,21 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement { ${repeat( this._userDefinedProperties, (column) => column.alias, - (column) => html`
  • ${column.header}: ${getPropertyValueByAlias(item, column.alias)}
  • `, + (column) => html` +
  • + ${column.header}: + ${when( + column.nameTemplate, + () => html` + + `, + () => html`${getPropertyValueByAlias(item, column.alias)}`, + )} +
  • + `, )} `; diff --git a/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts b/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts index 287e74da1e..e82195fe67 100644 --- a/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts +++ b/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts @@ -136,6 +136,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement { name: item.header, alias: item.alias, elementName: item.elementName, + labelTemplate: item.nameTemplate, allowSorting: true, }; }); diff --git a/src/packages/property-editors/collection/config/column/column-configuration.element.ts b/src/packages/property-editors/collection/config/column/column-configuration.element.ts index 146ab3ae0f..1a3d2e362a 100644 --- a/src/packages/property-editors/collection/config/column/column-configuration.element.ts +++ b/src/packages/property-editors/collection/config/column/column-configuration.element.ts @@ -82,6 +82,15 @@ export class UmbPropertyEditorUICollectionColumnConfigurationElement this.dispatchEvent(new UmbPropertyValueChangeEvent()); } + #onChangeNameTemplate(e: UUIInputEvent, configuration: UmbCollectionColumnConfiguration) { + this.value = this.value?.map( + (config): UmbCollectionColumnConfiguration => + config.alias === configuration.alias ? { ...config, nameTemplate: e.target.value as string } : config, + ); + + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + #onRemove(unique: string) { const newValue: Array = []; @@ -135,10 +144,10 @@ export class UmbPropertyEditorUICollectionColumnConfigurationElement + placeholder="Enter a label template..." + .value=${column.nameTemplate ?? ''} + @change=${(e: UUIInputEvent) => this.#onChangeNameTemplate(e, column)}>
    { + #value = new UmbObjectState(undefined); + readonly value = this.#value.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_UFM_RENDER_CONTEXT); + } + + getValue() { + return this.#value.getValue(); + } + + setValue(value: unknown | undefined) { + this.#value.setValue(value); + } +} + +export default UmbUfmRenderContext; + +export const UMB_UFM_RENDER_CONTEXT = new UmbContextToken('UmbUfmRenderContext'); diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts new file mode 100644 index 0000000000..f3e82c4067 --- /dev/null +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -0,0 +1,119 @@ +import type { UfmPlugin } from '../../plugins/marked-ufm.plugin.js'; +import { ufm } from '../../plugins/marked-ufm.plugin.js'; +import { UmbUfmRenderContext } from './ufm-render.context.js'; +import { css, customElement, nothing, property, unsafeHTML, until } from '@umbraco-cms/backoffice/external/lit'; +import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; +import { Marked } from '@umbraco-cms/backoffice/external/marked'; +import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; + +const UmbDomPurify = DOMPurify(window); +const UmbDomPurifyConfig: DOMPurify.Config = { + USE_PROFILES: { html: true }, + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: /^(?:ufm|umb|uui)-.*$/, + attributeNameCheck: /.+/, + allowCustomizedBuiltInElements: false, + }, +}; + +UmbDomPurify.addHook('afterSanitizeAttributes', function (node) { + // set all elements owning target to target=_blank + if ('target' in node) { + node.setAttribute('target', '_blank'); + } +}); + +const UmbMarked = new Marked({ + async: true, + gfm: true, + breaks: true, + hooks: { + postprocess: (markup) => { + return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string; + }, + }, +}); + +const elementName = 'umb-ufm-render'; + +@customElement(elementName) +export class UmbUfmRenderElement extends UmbLitElement { + #context = new UmbUfmRenderContext(this); + + @property({ type: Boolean }) + inline = false; + + @property() + markdown?: string; + + @property({ attribute: false }) + public set value(value: string | unknown | undefined) { + this.#context.setValue(value); + } + public get value(): string | unknown | undefined { + return this.#context.getValue(); + } + + constructor() { + super(); + + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => { + UmbMarked.use( + ufm( + controllers + .map((controller) => { + const ctrl = controller as unknown as UmbExtensionApiInitializer; + if (!ctrl.manifest || !ctrl.api) return; + return { + alias: ctrl.manifest.alias, + marker: ctrl.manifest.meta.marker, + render: ctrl.api.render, + }; + }) + .filter((x) => x) as Array, + ), + ); + this.requestUpdate('markdown'); + }); + } + + override render() { + return until(this.#renderMarkdown()); + } + + async #renderMarkdown() { + if (!this.markdown) return null; + const markup = !this.inline ? await UmbMarked.parse(this.markdown) : await UmbMarked.parseInline(this.markdown); + return markup ? unsafeHTML(markup) : nothing; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + color: var(--uui-color-text-alt); + } + + * { + max-width: 100%; + } + + pre { + overflow: auto; + } + `, + ]; +} + +export { UmbUfmRenderElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbUfmRenderElement; + } +} diff --git a/src/packages/ufm/index.ts b/src/packages/ufm/index.ts new file mode 100644 index 0000000000..6bf7af213f --- /dev/null +++ b/src/packages/ufm/index.ts @@ -0,0 +1,2 @@ +export * from './components/ufm-render/index.js'; +export * from './plugins/marked-ufm.plugin.js'; diff --git a/src/packages/ufm/manifests.ts b/src/packages/ufm/manifests.ts new file mode 100644 index 0000000000..3a89260097 --- /dev/null +++ b/src/packages/ufm/manifests.ts @@ -0,0 +1,22 @@ +import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'ufmComponent', + alias: 'Umb.Markdown.LabelValue', + name: 'Label Value Markdown Component', + api: () => import('./ufm-components/label-value.component.js'), + meta: { + marker: '=?', + }, + }, + { + type: 'ufmComponent', + alias: 'Umb.Markdown.Localize', + name: 'Localize Markdown Component', + api: () => import('./ufm-components/localize.component.js'), + meta: { + marker: '#', + }, + }, +]; diff --git a/src/packages/ufm/plugins/marked-ufm.plugin.ts b/src/packages/ufm/plugins/marked-ufm.plugin.ts new file mode 100644 index 0000000000..9ed749585d --- /dev/null +++ b/src/packages/ufm/plugins/marked-ufm.plugin.ts @@ -0,0 +1,41 @@ +import type { MarkedExtension, Tokens } from '@umbraco-cms/backoffice/external/marked'; + +export interface UfmPlugin { + alias: string; + marker: string; + render?: (token: Tokens.Generic) => string | undefined; +} + +export function ufm(plugins: Array = []): MarkedExtension { + return { + extensions: plugins.map(({ alias, marker, render }) => { + return { + name: alias, + level: 'inline', + start: (src: string) => { + const regex = new RegExp(`\\{${marker}`); + const match = src.match(regex); + return match ? match.index : -1; + }, + tokenizer(src: string): Tokens.Generic | undefined { + const pattern = `^(?`; +// } +// } + +// export { UmbUfmDocumentNameComponent as api }; diff --git a/src/packages/ufm/ufm-components/document-name.element.ts b/src/packages/ufm/ufm-components/document-name.element.ts new file mode 100644 index 0000000000..d4f59d15cc --- /dev/null +++ b/src/packages/ufm/ufm-components/document-name.element.ts @@ -0,0 +1,54 @@ +// import { UMB_UFM_RENDER_CONTEXT } from '@umbraco-cms/backoffice/components'; +// import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document'; +// import { customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +// import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +// const elementName = 'ufm-document-name'; + +// @customElement(elementName) +// export class UmbUfmDocumentNameElement extends UmbLitElement { +// @property() +// alias?: string; + +// @state() +// private _value?: unknown; + +// #documentRepository = new UmbDocumentItemRepository(this); + +// constructor() { +// super(); + +// this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => { +// this.observe( +// context.value, +// async (value) => { +// if (!value) return; + +// const unique = +// this.alias && typeof value === 'object' +// ? ((value as Record)[this.alias] as string) +// : (value as string); + +// if (!unique) return; + +// const { data } = await this.#documentRepository.requestItems([unique]); + +// this._value = data?.[0]?.name; +// }, +// 'observeValue', +// ); +// }); +// } + +// override render() { +// return this._value ?? this.alias; +// } +// } + +// export { UmbUfmDocumentNameElement as element }; + +// declare global { +// interface HTMLElementTagNameMap { +// [elementName]: UmbUfmDocumentNameElement; +// } +// } diff --git a/src/packages/ufm/ufm-components/label-value.component.ts b/src/packages/ufm/ufm-components/label-value.component.ts new file mode 100644 index 0000000000..00c58d0027 --- /dev/null +++ b/src/packages/ufm/ufm-components/label-value.component.ts @@ -0,0 +1,12 @@ +import { UmbUfmComponentBase } from './ufm-component-base.js'; +import type { Tokens } from '@umbraco-cms/backoffice/external/marked'; + +import './label-value.element.js'; + +export class UmbUfmLabelValueComponent extends UmbUfmComponentBase { + render(token: Tokens.Generic) { + return ``; + } +} + +export { UmbUfmLabelValueComponent as api }; diff --git a/src/packages/ufm/ufm-components/label-value.element.ts b/src/packages/ufm/ufm-components/label-value.element.ts new file mode 100644 index 0000000000..be54d466fc --- /dev/null +++ b/src/packages/ufm/ufm-components/label-value.element.ts @@ -0,0 +1,44 @@ +import { UMB_UFM_RENDER_CONTEXT } from '../components/ufm-render/index.js'; +import { customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +const elementName = 'ufm-label-value'; + +@customElement(elementName) +export class UmbUfmLabelValueElement extends UmbLitElement { + @property() + alias?: string; + + @state() + private _value?: unknown; + + constructor() { + super(); + + this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => { + this.observe( + context.value, + (value) => { + if (this.alias !== undefined && value !== undefined && typeof value === 'object') { + this._value = (value as Record)[this.alias]; + } else { + this._value = value; + } + }, + 'observeValue', + ); + }); + } + + override render() { + return this._value ?? `{${this.alias}}`; + } +} + +export { UmbUfmLabelValueElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbUfmLabelValueElement; + } +} diff --git a/src/packages/ufm/ufm-components/localize.component.ts b/src/packages/ufm/ufm-components/localize.component.ts new file mode 100644 index 0000000000..aacc2cd503 --- /dev/null +++ b/src/packages/ufm/ufm-components/localize.component.ts @@ -0,0 +1,10 @@ +import { UmbUfmComponentBase } from './ufm-component-base.js'; +import type { Tokens } from '@umbraco-cms/backoffice/external/marked'; + +export class UmbUfmLocalizeComponent extends UmbUfmComponentBase { + render(token: Tokens.Generic) { + return ``; + } +} + +export { UmbUfmLocalizeComponent as api }; diff --git a/src/packages/ufm/ufm-components/ufm-component-base.ts b/src/packages/ufm/ufm-components/ufm-component-base.ts new file mode 100644 index 0000000000..1109c69e76 --- /dev/null +++ b/src/packages/ufm/ufm-components/ufm-component-base.ts @@ -0,0 +1,7 @@ +import type { Tokens } from '@umbraco-cms/backoffice/external/marked'; +import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry'; + +export abstract class UmbUfmComponentBase implements UmbUfmComponentApi { + abstract render(token: Tokens.Generic): string | undefined; + destroy() {} +} diff --git a/src/packages/ufm/umbraco-package.ts b/src/packages/ufm/umbraco-package.ts new file mode 100644 index 0000000000..bd67768d43 --- /dev/null +++ b/src/packages/ufm/umbraco-package.ts @@ -0,0 +1,8 @@ +export const extensions = [ + { + name: 'Umbraco Flavored Markdown Bundle', + alias: 'Umb.Bundle.UmbracoFlavoredMarkdown', + type: 'bundle', + js: () => import('./manifests.js'), + }, +]; diff --git a/tsconfig.json b/tsconfig.json index fb0431b27f..dedd5f603b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -112,6 +112,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/themes": ["./src/packages/core/themes/index.ts"], "@umbraco-cms/backoffice/tiny-mce": ["./src/packages/tiny-mce/index.ts"], "@umbraco-cms/backoffice/tree": ["./src/packages/core/tree/index.ts"], + "@umbraco-cms/backoffice/ufm": ["./src/packages/ufm/index.ts"], "@umbraco-cms/backoffice/user-group": ["./src/packages/user/user-group/index.ts"], "@umbraco-cms/backoffice/user-permission": ["./src/packages/user/user-permission/index.ts"], "@umbraco-cms/backoffice/user": ["./src/packages/user/user/index.ts"], From 89682906f8ea240f274172b765f14ef82dba4c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Jun 2024 12:45:47 +0200 Subject: [PATCH 2/7] remove property to prevent too much re-rendering --- src/packages/ufm/components/ufm-render/ufm-render.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index f3e82c4067..a65b43b044 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -51,7 +51,6 @@ export class UmbUfmRenderElement extends UmbLitElement { @property() markdown?: string; - @property({ attribute: false }) public set value(value: string | unknown | undefined) { this.#context.setValue(value); } @@ -83,6 +82,7 @@ export class UmbUfmRenderElement extends UmbLitElement { } override render() { + console.log("re-render?", this) return until(this.#renderMarkdown()); } From 70355a99a7ce433ec4e22ede321b3c1d74996a80 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 26 Jun 2024 11:49:16 +0100 Subject: [PATCH 3/7] Conditionally render property description --- .../property-layout/property-layout.element.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/packages/core/property/property-layout/property-layout.element.ts b/src/packages/core/property/property-layout/property-layout.element.ts index 78ef7682db..1bbbdaae6a 100644 --- a/src/packages/core/property/property-layout/property-layout.element.ts +++ b/src/packages/core/property/property-layout/property-layout.element.ts @@ -60,7 +60,6 @@ export class UmbPropertyLayoutElement extends UmbLitElement { public invalid?: boolean; override render() { - const ufmValue = { alias: this.alias, label: this.label, description: this.description }; // TODO: Only show alias on label if user has access to DocumentType within settings: return html`
    @@ -69,9 +68,7 @@ export class UmbPropertyLayoutElement extends UmbLitElement { ${when(this.invalid, () => html`!`)} - - - + ${this.#renderDescription()}
    @@ -82,6 +79,12 @@ export class UmbPropertyLayoutElement extends UmbLitElement { `; } + #renderDescription() { + if (!this.description) return; + const ufmValue = { alias: this.alias, label: this.label, description: this.description }; + return html``; + } + static override styles = [ UmbTextStyles, css` From 865ac6831099ee0bc26cd7309529134f09021aec Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 26 Jun 2024 11:50:43 +0100 Subject: [PATCH 4/7] Remove console.log --- src/packages/ufm/components/ufm-render/ufm-render.element.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index a65b43b044..499fb509e8 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -82,7 +82,6 @@ export class UmbUfmRenderElement extends UmbLitElement { } override render() { - console.log("re-render?", this) return until(this.#renderMarkdown()); } From 52041295c12becf209526280fc092783a4c131bf Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 26 Jun 2024 11:56:15 +0100 Subject: [PATCH 5/7] Enforce the echo value to have a `=` marker (it is not optional) --- src/packages/ufm/manifests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/ufm/manifests.ts b/src/packages/ufm/manifests.ts index 3a89260097..b32b66dddf 100644 --- a/src/packages/ufm/manifests.ts +++ b/src/packages/ufm/manifests.ts @@ -7,7 +7,7 @@ export const manifests: Array = [ name: 'Label Value Markdown Component', api: () => import('./ufm-components/label-value.component.js'), meta: { - marker: '=?', + marker: '=', }, }, { From b806213a88085b604f4a787b36b9f8a66e80dbe9 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 26 Jun 2024 12:01:01 +0100 Subject: [PATCH 6/7] Sets the text color of the property description --- .../property/property-layout/property-layout.element.ts | 6 +++++- .../ufm/components/ufm-render/ufm-render.element.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/packages/core/property/property-layout/property-layout.element.ts b/src/packages/core/property/property-layout/property-layout.element.ts index 1bbbdaae6a..ba194a6e06 100644 --- a/src/packages/core/property/property-layout/property-layout.element.ts +++ b/src/packages/core/property/property-layout/property-layout.element.ts @@ -82,7 +82,7 @@ export class UmbPropertyLayoutElement extends UmbLitElement { #renderDescription() { if (!this.description) return; const ufmValue = { alias: this.alias, label: this.label, description: this.description }; - return html``; + return html``; } static override styles = [ @@ -131,6 +131,10 @@ export class UmbPropertyLayoutElement extends UmbLitElement { right: -30px; } + #description { + color: var(--uui-color-text-alt); + } + #editorColumn { margin-top: var(--uui-size-space-3); } diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index 499fb509e8..800d5a38b1 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -94,10 +94,6 @@ export class UmbUfmRenderElement extends UmbLitElement { static override styles = [ UmbTextStyles, css` - :host { - color: var(--uui-color-text-alt); - } - * { max-width: 100%; } From dfde9e12070f6d01f53b58595efc884cd387a67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Jun 2024 13:07:32 +0200 Subject: [PATCH 7/7] comment --- src/packages/ufm/components/ufm-render/ufm-render.element.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index 800d5a38b1..570669c6bc 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -51,6 +51,7 @@ export class UmbUfmRenderElement extends UmbLitElement { @property() markdown?: string; + // No reactive property declaration cause its causing a re-render that is not needed. This just works as a shortcut to set the values on the context. [NL] public set value(value: string | unknown | undefined) { this.#context.setValue(value); }