Feature: UFM component filters (#2246)
* UFM feature additions + refactor
- Re-structures "ufm-components" folder
- Adds `ufmFilter` extension type
- Adds `UmbUfmElementBase` to support reusing filters
* Code tidy-up
* Adds test for `{~contentPicker}` "ufm-content-name" syntax
* Exports `UfmPlugin` and `UfmToken` types
* Adds UFM filter extensions/functions
- Fallback
- Lowercase
- Strip HTML
- Title Case
- Truncate
- Uppercase
- Word Limit
This commit is contained in:
@@ -46,6 +46,7 @@ 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 { ManifestUfmFilter } from './ufm-filter.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';
|
||||
@@ -117,6 +118,7 @@ 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 './ufm-filter.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';
|
||||
@@ -210,6 +212,7 @@ export type ManifestTypes =
|
||||
| ManifestTreeItem
|
||||
| ManifestTreeStore
|
||||
| ManifestUfmComponent
|
||||
| ManifestUfmFilter
|
||||
| ManifestUserProfileApp
|
||||
| ManifestWorkspaceActionMenuItem
|
||||
| ManifestWorkspaceActions
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ManifestApi, UmbApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface UmbUfmFilterApi extends UmbApi {
|
||||
filter(...args: Array<unknown>): string | undefined | null;
|
||||
}
|
||||
|
||||
export interface MetaUfmFilter {
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export interface ManifestUfmFilter extends ManifestApi<UmbUfmFilterApi> {
|
||||
type: 'ufmFilter';
|
||||
meta: MetaUfmFilter;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
|
||||
import { UmbUfmComponentBase } from '../ufm-component-base.js';
|
||||
|
||||
import './content-name.element.js';
|
||||
|
||||
export class UmbUfmContentNameComponent extends UmbUfmComponentBase {
|
||||
render(token: UfmToken) {
|
||||
if (!token.text) return;
|
||||
|
||||
const attributes = super.getAttributes(token.text);
|
||||
return `<ufm-content-name ${attributes}></ufm-content-name>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmContentNameComponent as api };
|
||||
@@ -0,0 +1,74 @@
|
||||
import { UmbUfmElementBase } from '../ufm-element-base.js';
|
||||
import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js';
|
||||
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document';
|
||||
import { UmbMediaItemRepository } from '@umbraco-cms/backoffice/media';
|
||||
import { UmbMemberItemRepository } from '@umbraco-cms/backoffice/member';
|
||||
|
||||
const elementName = 'ufm-content-name';
|
||||
|
||||
@customElement(elementName)
|
||||
export class UmbUfmContentNameElement extends UmbUfmElementBase {
|
||||
@property()
|
||||
alias?: string;
|
||||
|
||||
#documentRepository?: UmbDocumentItemRepository;
|
||||
#mediaRepository?: UmbMediaItemRepository;
|
||||
#memberRepository?: UmbMemberItemRepository;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
context.value,
|
||||
async (value) => {
|
||||
const temp =
|
||||
this.alias && typeof value === 'object'
|
||||
? ((value as Record<string, unknown>)[this.alias] as string)
|
||||
: (value as string);
|
||||
|
||||
const entityType = Array.isArray(temp) && temp.length > 0 ? temp[0].type : null;
|
||||
const uniques = Array.isArray(temp) ? temp.map((x) => x.unique) : temp ? [temp] : [];
|
||||
|
||||
if (uniques?.length) {
|
||||
const repository = this.#getRepository(entityType);
|
||||
if (repository) {
|
||||
const { data } = await repository.requestItems(uniques);
|
||||
this.value = data ? data.map((item) => item.name).join(', ') : '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.value = '';
|
||||
},
|
||||
'observeValue',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#getRepository(entityType?: string | null) {
|
||||
switch (entityType) {
|
||||
case 'media':
|
||||
if (!this.#mediaRepository) this.#mediaRepository = new UmbMediaItemRepository(this);
|
||||
return this.#mediaRepository;
|
||||
|
||||
case 'member':
|
||||
if (!this.#memberRepository) this.#memberRepository = new UmbMemberItemRepository(this);
|
||||
return this.#memberRepository;
|
||||
|
||||
case 'document':
|
||||
default:
|
||||
if (!this.#documentRepository) this.#documentRepository = new UmbDocumentItemRepository(this);
|
||||
return this.#documentRepository;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmContentNameElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbUfmContentNameElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './ufm-render/index.js';
|
||||
export * from './ufm-component-base.js';
|
||||
export * from './ufm-element-base.js';
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
|
||||
import { UmbUfmComponentBase } from '../ufm-component-base.js';
|
||||
|
||||
import './label-value.element.js';
|
||||
|
||||
export class UmbUfmLabelValueComponent extends UmbUfmComponentBase {
|
||||
render(token: UfmToken) {
|
||||
if (!token.text) return;
|
||||
|
||||
const attributes = super.getAttributes(token.text);
|
||||
return `<ufm-label-value ${attributes}></ufm-label-value>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLabelValueComponent as api };
|
||||
@@ -1,17 +1,14 @@
|
||||
import { UMB_UFM_RENDER_CONTEXT } from '../components/ufm-render/index.js';
|
||||
import { customElement, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js';
|
||||
import { UmbUfmElementBase } from '../ufm-element-base.js';
|
||||
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
const elementName = 'ufm-label-value';
|
||||
|
||||
@customElement(elementName)
|
||||
export class UmbUfmLabelValueElement extends UmbLitElement {
|
||||
export class UmbUfmLabelValueElement extends UmbUfmElementBase {
|
||||
@property()
|
||||
alias?: string;
|
||||
|
||||
@state()
|
||||
private _value?: unknown;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -20,19 +17,15 @@ export class UmbUfmLabelValueElement extends UmbLitElement {
|
||||
context.value,
|
||||
(value) => {
|
||||
if (this.alias !== undefined && value !== undefined && typeof value === 'object') {
|
||||
this._value = (value as Record<string, unknown>)[this.alias];
|
||||
this.value = (value as Record<string, unknown>)[this.alias];
|
||||
} else {
|
||||
this._value = value;
|
||||
this.value = value;
|
||||
}
|
||||
},
|
||||
'observeValue',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._value !== undefined ? this._value : nothing;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLabelValueElement as element };
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
|
||||
import { UmbUfmComponentBase } from '../ufm-component-base.js';
|
||||
|
||||
import './localize.element.js';
|
||||
|
||||
export class UmbUfmLocalizeComponent extends UmbUfmComponentBase {
|
||||
render(token: UfmToken) {
|
||||
if (!token.text) return;
|
||||
|
||||
const attributes = super.getAttributes(token.text);
|
||||
return `<ufm-localize ${attributes}></ufm-localize>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLocalizeComponent as api };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { UmbUfmElementBase } from '../ufm-element-base.js';
|
||||
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
const elementName = 'ufm-localize';
|
||||
|
||||
@customElement(elementName)
|
||||
export class UmbUfmLocalizeElement extends UmbUfmElementBase {
|
||||
@property()
|
||||
public set alias(value: string | undefined) {
|
||||
if (!value) return;
|
||||
this.#alias = value;
|
||||
this.value = this.localize.term(value);
|
||||
}
|
||||
public get alias(): string | undefined {
|
||||
return this.#alias;
|
||||
}
|
||||
#alias?: string;
|
||||
}
|
||||
|
||||
export { UmbUfmLocalizeElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbUfmLocalizeElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestUfmComponent> = [
|
||||
{
|
||||
type: 'ufmComponent',
|
||||
alias: 'Umb.Markdown.LabelValue',
|
||||
name: 'Label Value UFM Component',
|
||||
api: () => import('./label-value/label-value.component.js'),
|
||||
meta: { marker: '=' },
|
||||
},
|
||||
{
|
||||
type: 'ufmComponent',
|
||||
alias: 'Umb.Markdown.Localize',
|
||||
name: 'Localize UFM Component',
|
||||
api: () => import('./localize/localize.component.js'),
|
||||
meta: { marker: '#' },
|
||||
},
|
||||
{
|
||||
type: 'ufmComponent',
|
||||
alias: 'Umb.Markdown.ContentName',
|
||||
name: 'Content Name UFM Component',
|
||||
api: () => import('./content-name/content-name.component.js'),
|
||||
meta: { marker: '~' },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
|
||||
import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export abstract class UmbUfmComponentBase implements UmbUfmComponentApi {
|
||||
protected getAttributes(text: string): string | null {
|
||||
if (!text) return null;
|
||||
|
||||
const pipeIndex = text.indexOf('|');
|
||||
|
||||
if (pipeIndex === -1) {
|
||||
return `alias="${text.trim()}"`;
|
||||
}
|
||||
|
||||
const alias = text.substring(0, pipeIndex).trim();
|
||||
const filters = text.substring(pipeIndex + 1).trim();
|
||||
|
||||
return Object.entries({ alias, filters })
|
||||
.map(([key, value]) => (value ? `${key}="${value.trim()}"` : null))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
abstract render(token: UfmToken): string | undefined;
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { UMB_UFM_CONTEXT } from '../contexts/ufm.context.js';
|
||||
import { nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name
|
||||
export abstract class UmbUfmElementBase extends UmbLitElement {
|
||||
#filterFuncArgs?: Array<{ alias: string; args: Array<string> }>;
|
||||
|
||||
@property()
|
||||
public set filters(value: string | undefined) {
|
||||
this.#filters = value;
|
||||
|
||||
this.#filterFuncArgs = value
|
||||
?.split('|')
|
||||
.filter((item) => item)
|
||||
.map((item) => {
|
||||
const [alias, ...args] = item.split(':').map((x) => x.trim());
|
||||
return { alias, args };
|
||||
});
|
||||
}
|
||||
public get filters(): string | undefined {
|
||||
return this.#filters;
|
||||
}
|
||||
#filters?: string;
|
||||
|
||||
@state()
|
||||
value?: unknown;
|
||||
|
||||
#ufmContext?: typeof UMB_UFM_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => {
|
||||
this.#ufmContext = ufmContext;
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.#ufmContext) return nothing;
|
||||
|
||||
let values = Array.isArray(this.value) ? this.value : [this.value];
|
||||
if (this.#filterFuncArgs) {
|
||||
for (const item of this.#filterFuncArgs) {
|
||||
const filter = this.#ufmContext.getFilterByAlias(item.alias);
|
||||
if (filter) {
|
||||
values = values.map((value) => filter(value, ...item.args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbUfmRenderContext extends UmbContextBase<UmbUfmRenderContext> {
|
||||
#value = new UmbObjectState<unknown>(undefined);
|
||||
|
||||
@@ -1,43 +1,8 @@
|
||||
import type { UfmPlugin } from '../../plugins/marked-ufm.plugin.js';
|
||||
import { ufm } from '../../plugins/marked-ufm.plugin.js';
|
||||
import { UMB_UFM_CONTEXT } from '../../contexts/ufm.context.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');
|
||||
}
|
||||
});
|
||||
|
||||
export 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';
|
||||
|
||||
@@ -59,26 +24,13 @@ export class UmbUfmRenderElement extends UmbLitElement {
|
||||
return this.#context.getValue();
|
||||
}
|
||||
|
||||
#ufmContext?: typeof UMB_UFM_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => {
|
||||
UmbMarked.use(
|
||||
ufm(
|
||||
controllers
|
||||
.map((controller) => {
|
||||
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmComponent>;
|
||||
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<UfmPlugin>,
|
||||
),
|
||||
);
|
||||
this.requestUpdate('markdown');
|
||||
this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => {
|
||||
this.#ufmContext = ufmContext;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,8 +39,8 @@ export class UmbUfmRenderElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
async #renderMarkdown() {
|
||||
if (!this.markdown) return null;
|
||||
const markup = !this.inline ? await UmbMarked.parse(this.markdown) : await UmbMarked.parseInline(this.markdown);
|
||||
if (!this.#ufmContext || !this.markdown) return null;
|
||||
const markup = await this.#ufmContext.parse(this.markdown, this.inline);
|
||||
return markup ? unsafeHTML(markup) : nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ufm.context.js';
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ManifestGlobalContext } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifest: ManifestGlobalContext = {
|
||||
type: 'globalContext',
|
||||
alias: 'Umb.GlobalContext.Ufm',
|
||||
name: 'UFM Context',
|
||||
api: () => import('./ufm.context.js'),
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ufm } from '../plugins/marked-ufm.plugin.js';
|
||||
import type { UfmPlugin } from '../plugins/marked-ufm.plugin.js';
|
||||
import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify';
|
||||
import { Marked } from '@umbraco-cms/backoffice/external/marked';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { ManifestUfmFilter, ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
export const UmbMarked = new Marked({
|
||||
async: true,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
hooks: {
|
||||
postprocess: (markup) => {
|
||||
return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type UmbUfmFilterType = {
|
||||
alias: string;
|
||||
filter: ((...args: Array<unknown>) => string | undefined | null) | undefined;
|
||||
};
|
||||
|
||||
export class UmbUfmContext extends UmbContextBase<UmbUfmContext> {
|
||||
#filters = new UmbArrayState<UmbUfmFilterType>([], (x) => x.alias);
|
||||
public readonly filters = this.#filters.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UMB_UFM_CONTEXT);
|
||||
|
||||
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => {
|
||||
UmbMarked.use(
|
||||
ufm(
|
||||
controllers
|
||||
.map((controller) => {
|
||||
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmComponent>;
|
||||
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<UfmPlugin>,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmFilter', [], undefined, (controllers) => {
|
||||
const filters = controllers
|
||||
.map((controller) => {
|
||||
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmFilter>;
|
||||
if (!ctrl.manifest || !ctrl.api) return null;
|
||||
return { alias: ctrl.manifest.meta.alias, filter: ctrl.api.filter };
|
||||
})
|
||||
.filter((x) => x) as Array<UmbUfmFilterType>;
|
||||
|
||||
this.#filters.setValue(filters);
|
||||
});
|
||||
}
|
||||
|
||||
public getFilterByAlias(alias: string) {
|
||||
return this.#filters.getValue().find((x) => x.alias === alias)?.filter;
|
||||
}
|
||||
|
||||
public async parse(markdown: string, inline: boolean) {
|
||||
return !inline ? await UmbMarked.parse(markdown) : await UmbMarked.parseInline(markdown);
|
||||
}
|
||||
}
|
||||
|
||||
export const UMB_UFM_CONTEXT = new UmbContextToken<UmbUfmContext>('UmbUfmContext');
|
||||
|
||||
export { UmbUfmContext as api };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmFallbackFilterApi extends UmbUfmFilterBase {
|
||||
filter(str: string, fallback: string) {
|
||||
return typeof str !== 'string' || str ? str : fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmFallbackFilterApi as api };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmLowercaseFilterApi extends UmbUfmFilterBase {
|
||||
filter(str?: string) {
|
||||
return str?.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLowercaseFilterApi as api };
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ManifestUfmFilter } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestUfmFilter> = [
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.Fallback',
|
||||
name: 'Fallback UFM Filter',
|
||||
api: () => import('./fallback.filter.js'),
|
||||
meta: { alias: 'fallback' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.Lowercase',
|
||||
name: 'Lowercase UFM Filter',
|
||||
api: () => import('./lowercase.filter.js'),
|
||||
meta: { alias: 'lowercase' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.StripHtml',
|
||||
name: 'Strip HTML UFM Filter',
|
||||
api: () => import('./strip-html.filter.js'),
|
||||
meta: { alias: 'strip-html' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.TitleCase',
|
||||
name: 'Title Case UFM Filter',
|
||||
api: () => import('./title-case.filter.js'),
|
||||
meta: { alias: 'title-case' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.Truncate',
|
||||
name: 'Truncate UFM Filter',
|
||||
api: () => import('./truncate.filter.js'),
|
||||
meta: { alias: 'truncate' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.Uppercase',
|
||||
name: 'Uppercase UFM Filter',
|
||||
api: () => import('./uppercase.filter.js'),
|
||||
meta: { alias: 'uppercase' },
|
||||
},
|
||||
{
|
||||
type: 'ufmFilter',
|
||||
alias: 'Umb.Filter.WordLimit',
|
||||
name: 'Word Limit UFM Filter',
|
||||
api: () => import('./word-limit.filter.js'),
|
||||
meta: { alias: 'word-limit' },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmStripHtmlFilterApi extends UmbUfmFilterBase {
|
||||
filter(value: string | { markup: string } | undefined | null) {
|
||||
if (!value) return '';
|
||||
|
||||
const markup = typeof value === 'object' && Object.hasOwn(value, 'markup') ? value.markup : (value as string);
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(markup, 'text/html');
|
||||
|
||||
return doc.body.textContent ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmStripHtmlFilterApi as api };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmTitleCaseFilterApi extends UmbUfmFilterBase {
|
||||
filter(str?: string) {
|
||||
return str?.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmTitleCaseFilterApi as api };
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmTruncateFilterApi extends UmbUfmFilterBase {
|
||||
filter(str: string, length: number, tail?: string) {
|
||||
if (typeof str !== 'string' || !str.length) return str;
|
||||
if (tail === 'false') tail = '';
|
||||
if (tail === 'true') tail = '…';
|
||||
tail = !tail && tail !== '' ? '…' : tail;
|
||||
|
||||
return str.slice(0, length).trim() + tail;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmTruncateFilterApi as api };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmUppercaseFilterApi extends UmbUfmFilterBase {
|
||||
filter(str?: string) {
|
||||
return str?.toLocaleUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmUppercaseFilterApi as api };
|
||||
@@ -0,0 +1,10 @@
|
||||
import { UmbUfmFilterBase } from '../types.js';
|
||||
|
||||
class UmbUfmWordLimitFilterApi extends UmbUfmFilterBase {
|
||||
filter(str: string, limit: number) {
|
||||
const words = str?.split(/\s+/) ?? [];
|
||||
return limit && words.length > limit ? words.slice(0, limit).join(' ') : str;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmWordLimitFilterApi as api };
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './components/ufm-render/index.js';
|
||||
export * from './plugins/marked-ufm.plugin.js';
|
||||
export * from './ufm-components/ufm-component-base.js';
|
||||
export * from './types.js';
|
||||
export * from './components/index.js';
|
||||
export * from './contexts/index.js';
|
||||
export * from './plugins/index.js';
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { manifest as ufmContext } from './contexts/manifest.js';
|
||||
import { manifests as ufmComponents } from './components/manifests.js';
|
||||
import { manifests as ufmFilters } from './filters/manifests.js';
|
||||
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export const manifests: Array<ManifestUfmComponent> = [
|
||||
{
|
||||
type: 'ufmComponent',
|
||||
alias: 'Umb.Markdown.LabelValue',
|
||||
name: 'Label Value UFM Component',
|
||||
api: () => import('./ufm-components/label-value.component.js'),
|
||||
meta: { marker: '=' },
|
||||
},
|
||||
{
|
||||
type: 'ufmComponent',
|
||||
alias: 'Umb.Markdown.Localize',
|
||||
name: 'Localize UFM Component',
|
||||
api: () => import('./ufm-components/localize.component.js'),
|
||||
meta: { marker: '#' },
|
||||
},
|
||||
];
|
||||
export const manifests: Array<ManifestTypes> = [ufmContext, ...ufmComponents, ...ufmFilters];
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ufm } from './marked-ufm.plugin.js';
|
||||
export type { UfmPlugin, UfmToken } from './marked-ufm.plugin.js';
|
||||
@@ -12,7 +12,8 @@ export interface UfmToken extends Tokens.Generic {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param plugins
|
||||
* @param {Array<UfmPlugin>} plugins - An array of UFM plugins.
|
||||
* @returns {MarkedExtension} A Marked extension object.
|
||||
*/
|
||||
export function ufm(plugins: Array<UfmPlugin> = []): MarkedExtension {
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { ufm } from './marked-ufm.plugin.js';
|
||||
import { UmbMarked } from '../index.js';
|
||||
import { UmbUfmLabelValueComponent } from '../ufm-components/label-value.component.js';
|
||||
import { UmbUfmLocalizeComponent } from '../ufm-components/localize.component.js';
|
||||
import { UmbMarked } from '../contexts/ufm.context.js';
|
||||
import { UmbUfmContentNameComponent } from '../components/content-name/content-name.component.js';
|
||||
import { UmbUfmLabelValueComponent } from '../components/label-value/label-value.component.js';
|
||||
import { UmbUfmLocalizeComponent } from '../components/localize/localize.component.js';
|
||||
|
||||
describe('UmbMarkedUfm', () => {
|
||||
describe('UFM parsing', () => {
|
||||
@@ -11,12 +12,18 @@ describe('UmbMarkedUfm', () => {
|
||||
{ ufm: '{= prop1}', expected: '<ufm-label-value alias="prop1"></ufm-label-value>' },
|
||||
{ ufm: '{= prop1 }', expected: '<ufm-label-value alias="prop1"></ufm-label-value>' },
|
||||
{ ufm: '{{=prop1}}', expected: '{<ufm-label-value alias="prop1"></ufm-label-value>}' },
|
||||
{ ufm: '{#general_add}', expected: '<umb-localize key="general_add"></umb-localize>' },
|
||||
{
|
||||
ufm: '{= prop1 | strip-html | truncate:30}',
|
||||
expected: '<ufm-label-value filters="strip-html | truncate:30" alias="prop1"></ufm-label-value>',
|
||||
},
|
||||
{ ufm: '{#general_add}', expected: '<ufm-localize alias="general_add"></ufm-localize>' },
|
||||
{ ufm: '{~contentPicker}', expected: '<ufm-content-name alias="contentPicker"></ufm-content-name>' },
|
||||
];
|
||||
|
||||
// Manually configuring the UFM components for testing.
|
||||
UmbMarked.use(
|
||||
ufm([
|
||||
{ alias: 'Umb.Markdown.ContentName', marker: '~', render: new UmbUfmContentNameComponent().render },
|
||||
{ alias: 'Umb.Markdown.LabelValue', marker: '=', render: new UmbUfmLabelValueComponent().render },
|
||||
{ alias: 'Umb.Markdown.Localize', marker: '#', render: new UmbUfmLocalizeComponent().render },
|
||||
]),
|
||||
|
||||
6
src/Umbraco.Web.UI.Client/src/packages/ufm/types.ts
Normal file
6
src/Umbraco.Web.UI.Client/src/packages/ufm/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export abstract class UmbUfmFilterBase implements UmbUfmFilterApi {
|
||||
abstract filter(...args: Array<unknown>): string | undefined | null;
|
||||
destroy() {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// import { UmbUfmComponentBase } from './ufm-component-base.js';
|
||||
// import type { Tokens } from '@umbraco-cms/backoffice/external/marked';
|
||||
|
||||
// import './document-name.element.js';
|
||||
|
||||
// export class UmbUfmDocumentNameComponent extends UmbUfmComponentBase {
|
||||
// render(token: Tokens.Generic) {
|
||||
// return `<ufm-document-name alias="${token.text}" debug></ufm-document-name>`;
|
||||
// }
|
||||
// }
|
||||
|
||||
// export { UmbUfmDocumentNameComponent as api };
|
||||
@@ -1,54 +0,0 @@
|
||||
// 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<string, unknown>)[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;
|
||||
// }
|
||||
// }
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
|
||||
import { UmbUfmComponentBase } from './ufm-component-base.js';
|
||||
|
||||
import './label-value.element.js';
|
||||
|
||||
export class UmbUfmLabelValueComponent extends UmbUfmComponentBase {
|
||||
render(token: UfmToken) {
|
||||
if (!token.text) return;
|
||||
return `<ufm-label-value alias="${token.text}"></ufm-label-value>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLabelValueComponent as api };
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
|
||||
import { UmbUfmComponentBase } from './ufm-component-base.js';
|
||||
|
||||
export class UmbUfmLocalizeComponent extends UmbUfmComponentBase {
|
||||
render(token: UfmToken) {
|
||||
if (!token.text) return;
|
||||
return `<umb-localize key="${token.text}"></umb-localize>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbUfmLocalizeComponent as api };
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
|
||||
import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export abstract class UmbUfmComponentBase implements UmbUfmComponentApi {
|
||||
abstract render(token: UfmToken): string | undefined;
|
||||
destroy() {}
|
||||
}
|
||||
Reference in New Issue
Block a user