This commit is contained in:
Niels Lyngsø
2023-12-05 11:31:20 +01:00
parent 01450b748f
commit 816132fcdc
272 changed files with 1499 additions and 1411 deletions

View File

@@ -3,9 +3,7 @@ import { css, html, customElement, state } from '@umbraco-cms/backoffice/externa
import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/section';
import type { UmbRoute, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/extension-registry';
import {
UmbExtensionManifestInitializer, createExtensionElement
} from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionManifestInitializer, createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-backoffice-main')
@@ -37,7 +35,7 @@ export class UmbBackofficeMainElement extends UmbLitElement {
this._sections = sections;
this._createRoutes();
},
'observeAllowedSections'
'observeAllowedSections',
);
}
}
@@ -47,21 +45,23 @@ export class UmbBackofficeMainElement extends UmbLitElement {
const oldValue = this._routes;
// TODO: Refactor this for re-use across the app where the routes are re-generated at any time.
this._routes = this._sections.filter(x => x.manifest).map((section) => {
const existingRoute = this._routes.find((r) => r.alias === section.alias);
if (existingRoute) {
return existingRoute;
} else {
return {
alias: section.alias,
path: this._routePrefix + (section.manifest as ManifestSection).meta.pathname,
component: () => createExtensionElement(section.manifest!, 'umb-section-default'),
setup: (component) => {
(component as UmbSectionElement).manifest = section.manifest as ManifestSection;
},
};
}
});
this._routes = this._sections
.filter((x) => x.manifest)
.map((section) => {
const existingRoute = this._routes.find((r) => r.alias === section.alias);
if (existingRoute) {
return existingRoute;
} else {
return {
alias: section.alias,
path: this._routePrefix + (section.manifest as ManifestSection).meta.pathname,
component: () => createExtensionElement(section.manifest!, 'umb-section-default'),
setup: (component) => {
(component as UmbSectionElement).manifest = section.manifest as ManifestSection;
},
};
}
});
if (this._sections.length > 0) {
this._routes.push({
@@ -79,7 +79,7 @@ export class UmbBackofficeMainElement extends UmbLitElement {
const section = this._sections.find((s) => this._routePrefix + (s.manifest as any).meta.pathname === currentPath);
if (!section) return;
await section.asPromise();
if(section.manifest) {
if (section.manifest) {
this._backofficeContext?.setActiveSectionAlias(section.alias);
this._provideSectionContext(section.manifest);
}
@@ -105,7 +105,9 @@ export class UmbBackofficeMainElement extends UmbLitElement {
:host {
background-color: var(--uui-color-background);
display: block;
height: calc(100% - 60px); // 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive.
height: calc(
100% - 60px
); // 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive.
}
`,
];

View File

@@ -169,7 +169,7 @@ export class UmbInstallerDatabaseElement extends UmbLitElement {
};
const { error } = await tryExecute(
InstallResource.postInstallValidateDatabase({ requestBody: databaseDetails })
InstallResource.postInstallValidateDatabase({ requestBody: databaseDetails }),
);
if (error) {
@@ -199,7 +199,7 @@ export class UmbInstallerDatabaseElement extends UmbLitElement {
this._installerContext.nextStep();
const { error } = await tryExecute(
InstallResource.postInstallSetup({ requestBody: this._installerContext.getData() })
InstallResource.postInstallSetup({ requestBody: this._installerContext.getData() }),
);
if (error) {
@@ -243,7 +243,7 @@ export class UmbInstallerDatabaseElement extends UmbLitElement {
}
result.push(
this._renderDatabaseName(this.databaseFormData.name ?? this._selectedDatabase.defaultDatabaseName ?? 'umbraco')
this._renderDatabaseName(this.databaseFormData.name ?? this._selectedDatabase.defaultDatabaseName ?? 'umbraco'),
);
if (this._selectedDatabase.requiresCredentials) {
@@ -271,19 +271,20 @@ export class UmbInstallerDatabaseElement extends UmbLitElement {
</uui-form-layout-item>
`;
private _renderDatabaseName = (value: string) => html` <uui-form-layout-item>
<uui-label for="database-name" slot="label" required>Database Name</uui-label>
<uui-input
type="text"
.value=${value}
id="database-name"
name="name"
label="Database name"
@input=${this._handleChange}
placeholder="umbraco"
required
required-message="Database name is required"></uui-input>
</uui-form-layout-item>`;
private _renderDatabaseName = (value: string) =>
html` <uui-form-layout-item>
<uui-label for="database-name" slot="label" required>Database Name</uui-label>
<uui-input
type="text"
.value=${value}
id="database-name"
name="name"
label="Database name"
@input=${this._handleChange}
placeholder="umbraco"
required
required-message="Database name is required"></uui-input>
</uui-form-layout-item>`;
private _renderCredentials = () => html`
<h2 class="uui-h4">Credentials</h2>

View File

@@ -60,8 +60,11 @@ export class UmbInstallerLayoutElement extends LitElement {
max-width: 1200px;
height: 100%;
max-height: 900px;
box-shadow: 0px 1.1px 3.7px rgba(0, 0, 0, 0.091), 0px 3.1px 10.1px rgba(0, 0, 0, 0.13),
0px 7.5px 24.4px rgba(0, 0, 0, 0.169), 0px 25px 81px rgba(0, 0, 0, 0.26);
box-shadow:
0px 1.1px 3.7px rgba(0, 0, 0, 0.091),
0px 3.1px 10.1px rgba(0, 0, 0, 0.13),
0px 7.5px 24.4px rgba(0, 0, 0, 0.169),
0px 25px 81px rgba(0, 0, 0, 0.26);
}
#grid {

View File

@@ -1,7 +1,10 @@
import { UmbInstallerContext } from '../installer.context.js';
import { html, type TemplateResult } from '@umbraco-cms/backoffice/external/lit';
export const installerContextProvider = (story: () => Node | string | TemplateResult, installerContext = new UmbInstallerContext()) => html`
export const installerContextProvider = (
story: () => Node | string | TemplateResult,
installerContext = new UmbInstallerContext(),
) => html`
<umb-context-provider
style="display: block;margin: 2rem 25%;padding: 1rem;border: 1px solid #ddd;"
key="umbInstallerContext"

View File

@@ -1,9 +1,9 @@
import { PathMatch } from "./model.js";
import { PathMatch } from './model.js';
export const CATCH_ALL_WILDCARD: string = "**";
export const TRAVERSE_FLAG: string = "\\.\\.\\/";
export const CATCH_ALL_WILDCARD: string = '**';
export const TRAVERSE_FLAG: string = '\\.\\.\\/';
export const PARAM_IDENTIFIER: RegExp = /:([^\\/]+)/g;
export const ROUTER_SLOT_TAG_NAME: string = "router-slot";
export const ROUTER_SLOT_TAG_NAME: string = 'router-slot';
export const GLOBAL_ROUTER_EVENTS_TARGET = window;
export const HISTORY_PATCH_NATIVE_KEY: string = `native`;
export const DEFAULT_PATH_MATCH: PathMatch = "prefix";
export const DEFAULT_PATH_MATCH: PathMatch = 'prefix';

View File

@@ -5,38 +5,48 @@ export interface IRouterSlot<D = any, P = any> extends HTMLElement {
readonly params: Params | null;
readonly match: IRouteMatch<D> | null;
routes: IRoute<D>[];
add: ((routes: IRoute<D>[], navigate?: boolean) => void);
clear: (() => void);
render: (() => Promise<void>);
constructAbsolutePath: ((path: PathFragment) => string);
add: (routes: IRoute<D>[], navigate?: boolean) => void;
clear: () => void;
render: () => Promise<void>;
constructAbsolutePath: (path: PathFragment) => string;
parent: IRouterSlot<P> | null | undefined;
queryParentRouterSlot: (() => IRouterSlot<P> | null);
queryParentRouterSlot: () => IRouterSlot<P> | null;
}
export type IRoutingInfo<D = any, P = any> = {
slot: IRouterSlot<D, P>,
match: IRouteMatch<D>
slot: IRouterSlot<D, P>;
match: IRouteMatch<D>;
};
export type CustomResolver<D = any, P = any> = ((info: IRoutingInfo<D>) => boolean | void | Promise<boolean> | Promise<void>);
export type Guard<D = any, P = any> = ((info: IRoutingInfo<D, P>) => boolean | Promise<boolean>);
export type Cancel = (() => boolean);
export type CustomResolver<D = any, P = any> = (
info: IRoutingInfo<D>,
) => boolean | void | Promise<boolean> | Promise<void>;
export type Guard<D = any, P = any> = (info: IRoutingInfo<D, P>) => boolean | Promise<boolean>;
export type Cancel = () => boolean;
export type PageComponent = HTMLElement | undefined;
export type ModuleResolver = Promise<{default: any; /*PageComponent*/}>;
export type Class<T extends PageComponent = PageComponent> = {new (...args: any[]): T;};
export type Component = Class | ModuleResolver | PageComponent | (() => Class) | (() => PromiseLike<Class>) | (() => PageComponent) | (() => PromiseLike<PageComponent>) | (() => ModuleResolver) | (() => PromiseLike<ModuleResolver>);
export type Setup<D = any> = ((component: PageComponent, info: IRoutingInfo<D>) => void);
export type ModuleResolver = Promise<{ default: any /*PageComponent*/ }>;
export type Class<T extends PageComponent = PageComponent> = { new (...args: any[]): T };
export type Component =
| Class
| ModuleResolver
| PageComponent
| (() => Class)
| (() => PromiseLike<Class>)
| (() => PageComponent)
| (() => PromiseLike<PageComponent>)
| (() => ModuleResolver)
| (() => PromiseLike<ModuleResolver>);
export type Setup<D = any> = (component: PageComponent, info: IRoutingInfo<D>) => void;
export type RouterTree<D = any, P = any> = {slot: IRouterSlot<D, P>} & {child?: RouterTree} | null | undefined;
export type PathMatch = "prefix" | "suffix" | "full" | "fuzzy";
export type RouterTree<D = any, P = any> = ({ slot: IRouterSlot<D, P> } & { child?: RouterTree }) | null | undefined;
export type PathMatch = 'prefix' | 'suffix' | 'full' | 'fuzzy';
/**
* The base route interface.
* D = the data type of the data
*/
export interface IRouteBase<D = any> {
// The path for the route fragment
path: PathFragment;
@@ -58,7 +68,6 @@ export interface IRouteBase<D = any> {
* Route type used for redirection.
*/
export interface IRedirectRoute<D = any> extends IRouteBase<D> {
// The paths the route should redirect to. Can either be relative or absolute.
redirectTo: string;
@@ -70,7 +79,6 @@ export interface IRedirectRoute<D = any> extends IRouteBase<D> {
* Route type used to resolve and stamp components.
*/
export interface IComponentRoute<D = any> extends IRouteBase<D> {
// The component loader (should return a module with a default export)
component: Component | PromiseLike<Component>;
@@ -82,7 +90,6 @@ export interface IComponentRoute<D = any> extends IRouteBase<D> {
* Route type used to take control of how the route should resolve.
*/
export interface IResolverRoute<D = any> extends IRouteBase<D> {
// A custom resolver that handles the route change
resolve: CustomResolver;
}
@@ -90,13 +97,13 @@ export interface IResolverRoute<D = any> extends IRouteBase<D> {
export type IRoute<D = any> = IRedirectRoute<D> | IComponentRoute<D> | IResolverRoute<D>;
export type PathFragment = string;
export type IPathFragments = {
consumed: PathFragment,
rest: PathFragment
}
consumed: PathFragment;
rest: PathFragment;
};
export interface IRouteMatch<D = any> {
route: IRoute<D>;
params: Params,
params: Params;
fragments: IPathFragments;
match: RegExpMatchArray;
}
@@ -104,57 +111,56 @@ export interface IRouteMatch<D = any> {
export type PushStateEvent = CustomEvent<null>;
export type ReplaceStateEvent = CustomEvent<null>;
export type ChangeStateEvent = CustomEvent<null>;
export type WillChangeStateEvent = CustomEvent<{ url?: string | null, eventName: GlobalRouterEvent}>;
export type WillChangeStateEvent = CustomEvent<{ url?: string | null; eventName: GlobalRouterEvent }>;
export type NavigationStartEvent<D = any> = CustomEvent<IRoutingInfo<D>>;
export type NavigationSuccessEvent<D = any> = CustomEvent<IRoutingInfo<D>>;
export type NavigationCancelEvent<D = any> = CustomEvent<IRoutingInfo<D>>;
export type NavigationErrorEvent<D = any> = CustomEvent<IRoutingInfo<D>>;
export type NavigationEndEvent<D = any> = CustomEvent<IRoutingInfo<D>>;
export type Params = {[key: string]: string};
export type Query = {[key: string]: string};
export type Params = { [key: string]: string };
export type Query = { [key: string]: string };
export type EventListenerSubscription = (() => void);
export type EventListenerSubscription = () => void;
/**
* RouterSlot related events.
*/
export type RouterSlotEvent = "changestate";
export type RouterSlotEvent = 'changestate';
/**
* History related events.
*/
export type GlobalRouterEvent =
// An event triggered when a new state is added to the history.
"pushstate"
// An event triggered when a new state is added to the history.
| 'pushstate'
// An event triggered when the current state is replaced in the history.
| "replacestate"
| 'replacestate'
// An event triggered when a state in the history is popped from the history.
| "popstate"
| 'popstate'
// An event triggered when the state changes (eg. pop, push and replace)
| "changestate"
| 'changestate'
// A cancellable event triggered before the history state changes.
| "willchangestate"
| 'willchangestate'
// An event triggered when navigation starts.
| "navigationstart"
| 'navigationstart'
// An event triggered when navigation is canceled. This is due to a route guard returning false during navigation.
| "navigationcancel"
| 'navigationcancel'
// An event triggered when navigation fails due to an unexpected error.
| "navigationerror"
| 'navigationerror'
// An event triggered when navigation successfully completes.
| "navigationsuccess"
| 'navigationsuccess'
// An event triggered when navigation ends.
| "navigationend";
| 'navigationend';
export interface ISlashOptions {
start: boolean;
@@ -164,15 +170,15 @@ export interface ISlashOptions {
/* Extend the global event handlers map with the router related events */
declare global {
interface GlobalEventHandlersEventMap {
"pushstate": PushStateEvent,
"replacestate": ReplaceStateEvent,
"popstate": PopStateEvent,
"changestate": ChangeStateEvent,
"navigationstart": NavigationStartEvent,
"navigationend": NavigationEndEvent,
"navigationsuccess": NavigationSuccessEvent,
"navigationcancel": NavigationCancelEvent,
"navigationerror": NavigationErrorEvent,
"willchangestate": WillChangeStateEvent
pushstate: PushStateEvent;
replacestate: ReplaceStateEvent;
popstate: PopStateEvent;
changestate: ChangeStateEvent;
navigationstart: NavigationStartEvent;
navigationend: NavigationEndEvent;
navigationsuccess: NavigationSuccessEvent;
navigationcancel: NavigationCancelEvent;
navigationerror: NavigationErrorEvent;
willchangestate: WillChangeStateEvent;
}
}
}

View File

@@ -1,4 +1,4 @@
import { GLOBAL_ROUTER_EVENTS_TARGET, ROUTER_SLOT_TAG_NAME } from "./config.js";
import { GLOBAL_ROUTER_EVENTS_TARGET, ROUTER_SLOT_TAG_NAME } from './config.js';
import {
Cancel,
EventListenerSubscription,
@@ -11,7 +11,7 @@ import {
Params,
PathFragment,
RouterSlotEvent,
} from "./model.js";
} from './model.js';
import {
addListener,
constructAbsolutePath,
@@ -28,9 +28,9 @@ import {
removeListeners,
resolvePageComponent,
shouldNavigate,
} from "./util.js";
} from './util.js';
const template = document.createElement("template");
const template = document.createElement('template');
template.innerHTML = `<slot></slot>`;
// Patches the history object and ensures the correct events.
@@ -44,10 +44,7 @@ ensureAnchorHistory();
* @slot - Default content.
* @event changestate - Dispatched when the router slot state changes.
*/
export class RouterSlot<D = any, P = any>
extends HTMLElement
implements IRouterSlot<D, P>
{
export class RouterSlot<D = any, P = any> extends HTMLElement implements IRouterSlot<D, P> {
/**
* Listeners on the router.
*/
@@ -131,7 +128,7 @@ export class RouterSlot<D = any, P = any>
constructor() {
super();
this.addEventListener("router-slot:capture-parent", (e: any) => {
this.addEventListener('router-slot:capture-parent', (e: any) => {
e.stopPropagation();
e.detail.parent = this;
});
@@ -139,7 +136,7 @@ export class RouterSlot<D = any, P = any>
this.render = this.render.bind(this);
// Attach the template
const shadow = this.attachShadow({ mode: "open" });
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
}
@@ -149,7 +146,7 @@ export class RouterSlot<D = any, P = any>
connectedCallback() {
// Do not query a parent if the parent has been set from the outside.
if (!this._lockParent) {
const captureParentEvent = new CustomEvent("router-slot:capture-parent", {
const captureParentEvent = new CustomEvent('router-slot:capture-parent', {
composed: true,
bubbles: true,
detail: { parent: null },
@@ -237,9 +234,7 @@ export class RouterSlot<D = any, P = any>
// Either choose the parent fragment or the current path if no parent exists.
// The root router slot will always use the entire path.
const pathFragment =
this.parent != null && this.parent.fragments != null
? this.parent.fragments.rest
: pathWithoutBasePath();
this.parent != null && this.parent.fragments != null ? this.parent.fragments.rest : pathWithoutBasePath();
// Route to the path
await this.renderPath(pathFragment);
@@ -253,17 +248,9 @@ export class RouterSlot<D = any, P = any>
this.listeners.push(
this.parent != null
? // Attach child router listeners
addListener<Event, RouterSlotEvent>(
this.parent,
"changestate",
this.render
)
addListener<Event, RouterSlotEvent>(this.parent, 'changestate', this.render)
: // Add global listeners.
addListener<Event, GlobalRouterEvent>(
GLOBAL_ROUTER_EVENTS_TARGET,
"changestate",
this.render
)
addListener<Event, GlobalRouterEvent>(GLOBAL_ROUTER_EVENTS_TARGET, 'changestate', this.render),
);
}
@@ -328,12 +315,14 @@ export class RouterSlot<D = any, P = any>
// while we are about to navigate we have to cancel.
let navigationInvalidated = false;
const cancelNavigation = () => (navigationInvalidated = true);
const removeChangeListener: EventListenerSubscription = addListener<
Event,
GlobalRouterEvent
>(GLOBAL_ROUTER_EVENTS_TARGET, "changestate", cancelNavigation, {
once: true,
});
const removeChangeListener: EventListenerSubscription = addListener<Event, GlobalRouterEvent>(
GLOBAL_ROUTER_EVENTS_TARGET,
'changestate',
cancelNavigation,
{
once: true,
},
);
// Cleans up the routing by removing listeners and restoring the match from before
const cleanup = () => {
@@ -343,13 +332,13 @@ export class RouterSlot<D = any, P = any>
// Cleans up and dispatches a global event that a navigation was cancelled.
const cancel: Cancel = () => {
cleanup();
dispatchGlobalRouterEvent("navigationcancel", info);
dispatchGlobalRouterEvent("navigationend", info);
dispatchGlobalRouterEvent('navigationcancel', info);
dispatchGlobalRouterEvent('navigationend', info);
return false;
};
// Dispatch globally that a navigation has started
dispatchGlobalRouterEvent("navigationstart", info);
dispatchGlobalRouterEvent('navigationstart', info);
// Check whether the guards allow us to go to the new route.
if (route.guards != null) {
@@ -395,7 +384,7 @@ export class RouterSlot<D = any, P = any>
// We do this to ensure that we can find the match in the connectedCallback of the page.
this._routeMatch = match;
if(page) {
if (page) {
// Append the new page
this.appendChild(page);
}
@@ -413,14 +402,14 @@ export class RouterSlot<D = any, P = any>
// Dispatch globally that a navigation has ended.
if (navigate) {
dispatchGlobalRouterEvent("navigationsuccess", info);
dispatchGlobalRouterEvent("navigationend", info);
dispatchGlobalRouterEvent('navigationsuccess', info);
dispatchGlobalRouterEvent('navigationend', info);
}
return navigate;
} catch (e) {
dispatchGlobalRouterEvent("navigationerror", info);
dispatchGlobalRouterEvent("navigationend", info);
dispatchGlobalRouterEvent('navigationerror', info);
dispatchGlobalRouterEvent('navigationend', info);
throw e;
}
}
@@ -430,6 +419,6 @@ window.customElements.define(ROUTER_SLOT_TAG_NAME, RouterSlot);
declare global {
interface HTMLElementTagNameMap {
"router-slot": RouterSlot;
'router-slot': RouterSlot;
}
}

View File

@@ -2,11 +2,12 @@
* Hook up a click listener to the window that, for all anchor tags
* that has a relative HREF, uses the history API instead.
*/
export function ensureAnchorHistory () {
window.addEventListener("click", (e: MouseEvent) => {
export function ensureAnchorHistory() {
window.addEventListener('click', (e: MouseEvent) => {
// Find the target by using the composed path to get the element through the shadow boundaries.
const $anchor = ("composedPath" in e as any) ? e.composedPath().find($elem => $elem instanceof HTMLAnchorElement) : e.target;
const $anchor = (('composedPath' in e) as any)
? e.composedPath().find(($elem) => $elem instanceof HTMLAnchorElement)
: e.target;
// Abort if the event is not about the anchor tag
if ($anchor == null || !($anchor instanceof HTMLAnchorElement)) {
@@ -20,9 +21,11 @@ export function ensureAnchorHistory () {
// - The HREF is relative to the origin of the current location.
// - The target is targeting the current frame.
// - The anchor doesn't have the attribute [data-router-slot]="disabled"
if (!href.startsWith(location.origin) ||
($anchor.target !== "" && $anchor.target !== "_self") ||
$anchor.dataset["routerSlot"] === "disabled") {
if (
!href.startsWith(location.origin) ||
($anchor.target !== '' && $anchor.target !== '_self') ||
$anchor.dataset['routerSlot'] === 'disabled'
) {
return;
}
@@ -33,6 +36,6 @@ export function ensureAnchorHistory () {
e.preventDefault();
// Change the history!
history.pushState(null, "", path);
history.pushState(null, '', path);
});
}
}

View File

@@ -1,13 +1,13 @@
import { GLOBAL_ROUTER_EVENTS_TARGET } from "../config";
import { EventListenerSubscription, GlobalRouterEvent, IRoute, IRoutingInfo } from "../model";
import { GLOBAL_ROUTER_EVENTS_TARGET } from '../config';
import { EventListenerSubscription, GlobalRouterEvent, IRoute, IRoutingInfo } from '../model';
/**
* Dispatches a did change route event.
* @param $elem
* @param {IRoute} detail
*/
export function dispatchRouteChangeEvent<D = any> ($elem: HTMLElement, detail: IRoutingInfo<D>) {
$elem.dispatchEvent(new CustomEvent("changestate", {detail}));
export function dispatchRouteChangeEvent<D = any>($elem: HTMLElement, detail: IRoutingInfo<D>) {
$elem.dispatchEvent(new CustomEvent('changestate', { detail }));
}
/**
@@ -15,8 +15,8 @@ export function dispatchRouteChangeEvent<D = any> ($elem: HTMLElement, detail: I
* @param name
* @param detail
*/
export function dispatchGlobalRouterEvent<D = any> (name: GlobalRouterEvent, detail?: IRoutingInfo<D>) {
GLOBAL_ROUTER_EVENTS_TARGET.dispatchEvent(new CustomEvent(name, {detail}));
export function dispatchGlobalRouterEvent<D = any>(name: GlobalRouterEvent, detail?: IRoutingInfo<D>) {
GLOBAL_ROUTER_EVENTS_TARGET.dispatchEvent(new CustomEvent(name, { detail }));
// if ("debugRouterSlot" in window) {
// console.log(`%c [router-slot]: ${name}`, `color: #286ee0`, detail);
// }
@@ -29,21 +29,22 @@ export function dispatchGlobalRouterEvent<D = any> (name: GlobalRouterEvent, det
* @param listener
* @param options
*/
export function addListener<T extends Event, eventType extends string> ($elem: EventTarget,
type: eventType[] | eventType,
listener: ((e: T) => void),
options?: boolean | AddEventListenerOptions): EventListenerSubscription {
export function addListener<T extends Event, eventType extends string>(
$elem: EventTarget,
type: eventType[] | eventType,
listener: (e: T) => void,
options?: boolean | AddEventListenerOptions,
): EventListenerSubscription {
const types = Array.isArray(type) ? type : [type];
types.forEach(t => $elem.addEventListener(t, listener as EventListenerOrEventListenerObject, options));
return () => types.forEach(
t => $elem.removeEventListener(t, listener as EventListenerOrEventListenerObject, options));
types.forEach((t) => $elem.addEventListener(t, listener as EventListenerOrEventListenerObject, options));
return () =>
types.forEach((t) => $elem.removeEventListener(t, listener as EventListenerOrEventListenerObject, options));
}
/**
* Removes the event listeners in the array.
* @param listeners
*/
export function removeListeners (listeners: EventListenerSubscription[]) {
listeners.forEach(unsub => unsub());
export function removeListeners(listeners: EventListenerSubscription[]) {
listeners.forEach((unsub) => unsub());
}

View File

@@ -1,47 +1,44 @@
import { GLOBAL_ROUTER_EVENTS_TARGET, HISTORY_PATCH_NATIVE_KEY } from "../config";
import { GlobalRouterEvent } from "../model";
import { dispatchGlobalRouterEvent } from "./events";
import { GLOBAL_ROUTER_EVENTS_TARGET, HISTORY_PATCH_NATIVE_KEY } from '../config';
import { GlobalRouterEvent } from '../model';
import { dispatchGlobalRouterEvent } from './events';
// Mapping a history functions to the events they are going to dispatch.
export const historyPatches: [string, GlobalRouterEvent[]][] = [
["pushState", ["pushstate", "changestate"]],
["replaceState", ["replacestate", "changestate"]],
["forward", ["pushstate", "changestate"]],
["go", ["pushstate", "changestate"]],
['pushState', ['pushstate', 'changestate']],
['replaceState', ['replacestate', 'changestate']],
['forward', ['pushstate', 'changestate']],
['go', ['pushstate', 'changestate']],
// We need to handle the popstate a little differently when it comes to the change state event.
["back", ["popstate"]],
// We need to handle the popstate a little differently when it comes to the change state event.
['back', ['popstate']],
];
/**
* Patches the history object by ensuring correct events are dispatches when the history changes.
*/
export function ensureHistoryEvents() {
for (const [name, events] of historyPatches) {
for (const event of events) {
attachCallback(history, name, event);
}
}
for (const [name, events] of historyPatches) {
for (const event of events) {
attachCallback(history, name, event);
}
}
// The popstate is the only event natively dispatched when using the hardware buttons.
// Therefore we need to handle this case a little different. To ensure the changestate event
// is fired also when the hardware back button is used, we make sure to listen for the popstate
// event and dispatch a change state event right after. The reason for the setTimeout is because we
// want the popstate event to bubble up before the changestate event is dispatched.
window.addEventListener("popstate", (e: PopStateEvent) => {
// The popstate is the only event natively dispatched when using the hardware buttons.
// Therefore we need to handle this case a little different. To ensure the changestate event
// is fired also when the hardware back button is used, we make sure to listen for the popstate
// event and dispatch a change state event right after. The reason for the setTimeout is because we
// want the popstate event to bubble up before the changestate event is dispatched.
window.addEventListener('popstate', (e: PopStateEvent) => {
// Check if the state should be allowed to change
if (shouldCancelChangeState({ eventName: 'popstate' })) {
e.preventDefault();
e.stopPropagation();
return;
}
// Check if the state should be allowed to change
if (shouldCancelChangeState({eventName: "popstate"})) {
e.preventDefault();
e.stopPropagation();
return;
}
// Dispatch the global router event to change the routes after the popstate has bubbled up
setTimeout(() => dispatchGlobalRouterEvent("changestate"), 0);
}
);
// Dispatch the global router event to change the routes after the popstate has bubbled up
setTimeout(() => dispatchGlobalRouterEvent('changestate'), 0);
});
}
/**
@@ -52,20 +49,19 @@ export function ensureHistoryEvents() {
* @param eventName
*/
export function attachCallback(obj: any, functionName: string, eventName: GlobalRouterEvent) {
const func = obj[functionName];
saveNativeFunction(obj, functionName, func);
obj[functionName] = (...args: any[]) => {
const func = obj[functionName];
saveNativeFunction(obj, functionName, func);
obj[functionName] = (...args: any[]) => {
// If its push/replace state we want to send the url to the should cancel change state event
const url = args.length > 2 ? args[2] : null;
// If its push/replace state we want to send the url to the should cancel change state event
const url = args.length > 2 ? args[2] : null;
// Check if the state should be allowed to change
if (shouldCancelChangeState({ url, eventName })) return;
// Check if the state should be allowed to change
if (shouldCancelChangeState({url, eventName})) return;
// Navigate
func.apply(obj, args);
dispatchGlobalRouterEvent(eventName)
};
// Navigate
func.apply(obj, args);
dispatchGlobalRouterEvent(eventName);
};
}
/**
@@ -74,37 +70,38 @@ export function attachCallback(obj: any, functionName: string, eventName: Global
* @param name
* @param func
*/
export function saveNativeFunction(obj: any, name: string, func: (() => void)) {
export function saveNativeFunction(obj: any, name: string, func: () => void) {
// Ensure that the native object exists.
if (obj[HISTORY_PATCH_NATIVE_KEY] == null) {
obj[HISTORY_PATCH_NATIVE_KEY] = {};
}
// Ensure that the native object exists.
if (obj[HISTORY_PATCH_NATIVE_KEY] == null) {
obj[HISTORY_PATCH_NATIVE_KEY] = {};
}
// Save the native function.
obj[HISTORY_PATCH_NATIVE_KEY][`${name}`] = func.bind(obj);
// Save the native function.
obj[HISTORY_PATCH_NATIVE_KEY][`${name}`] = func.bind(obj);
}
/**
* Dispatches and event and returns whether the state change should be cancelled.
* The state will be considered as cancelled if the "willChangeState" event was cancelled.
*/
function shouldCancelChangeState(data: { url?: string | null, eventName: GlobalRouterEvent }): boolean {
return !GLOBAL_ROUTER_EVENTS_TARGET.dispatchEvent(new CustomEvent("willchangestate", {
cancelable: true,
detail: data
}));
function shouldCancelChangeState(data: { url?: string | null; eventName: GlobalRouterEvent }): boolean {
return !GLOBAL_ROUTER_EVENTS_TARGET.dispatchEvent(
new CustomEvent('willchangestate', {
cancelable: true,
detail: data,
}),
);
}
// Expose the native history functions.
declare global {
interface History {
"native": {
"back": ((distance?: any) => void);
"forward": ((distance?: any) => void);
"go": ((delta?: any) => void);
"pushState": ((data: any, title?: string, url?: string | null) => void);
"replaceState": ((data: any, title?: string, url?: string | null) => void);
}
}
}
interface History {
native: {
back: (distance?: any) => void;
forward: (distance?: any) => void;
go: (delta?: any) => void;
pushState: (data: any, title?: string, url?: string | null) => void;
replaceState: (data: any, title?: string, url?: string | null) => void;
};
}
}

View File

@@ -1,6 +1,19 @@
import { CATCH_ALL_WILDCARD, DEFAULT_PATH_MATCH, PARAM_IDENTIFIER, TRAVERSE_FLAG } from "../config";
import { IComponentRoute, IRedirectRoute, IResolverRoute, IRoute, IRouteMatch, IRouterSlot, ModuleResolver, PageComponent, Params, PathFragment, RouterTree, IRoutingInfo } from "../model";
import { constructPathWithBasePath, path as getPath, queryString, stripSlash } from "./url";
import { CATCH_ALL_WILDCARD, DEFAULT_PATH_MATCH, PARAM_IDENTIFIER, TRAVERSE_FLAG } from '../config';
import {
IComponentRoute,
IRedirectRoute,
IResolverRoute,
IRoute,
IRouteMatch,
IRouterSlot,
ModuleResolver,
PageComponent,
Params,
PathFragment,
RouterTree,
IRoutingInfo,
} from '../model';
import { constructPathWithBasePath, path as getPath, queryString, stripSlash } from './url';
/**
* Determines whether the path is active.
@@ -8,8 +21,8 @@ import { constructPathWithBasePath, path as getPath, queryString, stripSlash } f
* @param path
* @param fullPath
*/
export function isPathActive (path: string | PathFragment, fullPath: string = getPath()): boolean {
return new RegExp(`^${stripSlash(path)}(\/|$)`, "gm").test(stripSlash(fullPath));
export function isPathActive(path: string | PathFragment, fullPath: string = getPath()): boolean {
return new RegExp(`^${stripSlash(path)}(\/|$)`, 'gm').test(stripSlash(fullPath));
}
/**
@@ -17,16 +30,17 @@ export function isPathActive (path: string | PathFragment, fullPath: string = ge
* @param route
* @param path
*/
export function matchRoute<D = any> (route: IRoute<D>, path: string | PathFragment): IRouteMatch<D> | null {
export function matchRoute<D = any>(route: IRoute<D>, path: string | PathFragment): IRouteMatch<D> | null {
// We start by preparing the route path by replacing the param names with a regex that matches everything
// until either the end of the path or the next "/". While replacing the param placeholders we make sure
// to store the names of the param placeholders.
const paramNames: string[] = [];
const routePath = stripSlash(route.path.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => {
paramNames.push(args[0]);
return `([^\/]+)`;
}));
const routePath = stripSlash(
route.path.replace(PARAM_IDENTIFIER, (substring: string, ...args: string[]) => {
paramNames.push(args[0]);
return `([^\/]+)`;
}),
);
// Construct the regex to match with the path or fragment
// If path is wildcard:
@@ -45,19 +59,26 @@ export function matchRoute<D = any> (route: IRoute<D>, path: string | PathFragme
// - We start the match with .*? to allow anything to be in front of what we are trying to match.
// - We end the match with .*? to allow anything to be after what we are trying to match.
// All matches starts with ^ to make sure the match is done from the beginning of the path.
const regex = route.path === CATCH_ALL_WILDCARD || (route.path.length === 0 && route.pathMatch != "full" ) ? /^/ : (() => {
switch (route.pathMatch || DEFAULT_PATH_MATCH) {
case "full": return new RegExp(`^${routePath}\/?$`);
case "suffix": return new RegExp(`^.*?${routePath}\/?$`);
case "fuzzy": return new RegExp(`^.*?${routePath}.*?$`);
case "prefix": default: return new RegExp(`^[\/]?${routePath}(?:\/|$)`);
}
})();
const regex =
route.path === CATCH_ALL_WILDCARD || (route.path.length === 0 && route.pathMatch != 'full')
? /^/
: (() => {
switch (route.pathMatch || DEFAULT_PATH_MATCH) {
case 'full':
return new RegExp(`^${routePath}\/?$`);
case 'suffix':
return new RegExp(`^.*?${routePath}\/?$`);
case 'fuzzy':
return new RegExp(`^.*?${routePath}.*?$`);
case 'prefix':
default:
return new RegExp(`^[\/]?${routePath}(?:\/|$)`);
}
})();
// Check if there's a match
const match = path.match(regex);
if (match != null) {
// Match the param names with the matches. The matches starts from index 1 which is the
// reason why we add 1. match[0] is the entire string.
const params = paramNames.reduce((acc: Params, name: string, i: number) => {
@@ -75,12 +96,11 @@ export function matchRoute<D = any> (route: IRoute<D>, path: string | PathFragme
params,
fragments: {
consumed,
rest
}
rest,
},
};
}
return null;
}
@@ -89,7 +109,7 @@ export function matchRoute<D = any> (route: IRoute<D>, path: string | PathFragme
* @param routes
* @param path
*/
export function matchRoutes<D = any> (routes: IRoute<D>[], path: string | PathFragment): IRouteMatch<D> | null {
export function matchRoutes<D = any>(routes: IRoute<D>[], path: string | PathFragment): IRouteMatch<D> | null {
for (const route of routes) {
const match = matchRoute(route, path);
if (match != null) {
@@ -106,15 +126,13 @@ export function matchRoutes<D = any> (routes: IRoute<D>[], path: string | PathFr
* @param route
* @param info
*/
export async function resolvePageComponent (route: IComponentRoute, info: IRoutingInfo): Promise<PageComponent> {
export async function resolvePageComponent(route: IComponentRoute, info: IRoutingInfo): Promise<PageComponent> {
// Figure out if the component were given as an import or class.
let cmp = route.component;
if (cmp instanceof Function) {
try {
cmp = (cmp as Function)();
} catch (err) {
// The invocation most likely failed because the function is a class.
// If it failed due to the "new" keyword not being used, the error will be of type "TypeError".
// This is the most reliable way to check whether the provided function is a class or a function.
@@ -147,24 +165,23 @@ export async function resolvePageComponent (route: IComponentRoute, info: IRouti
* Determines if a route is a redirect route.
* @param route
*/
export function isRedirectRoute (route: IRoute): route is IRedirectRoute {
return "redirectTo" in route;
export function isRedirectRoute(route: IRoute): route is IRedirectRoute {
return 'redirectTo' in route;
}
/**
* Determines if a route is a resolver route.
* @param route
*/
export function isResolverRoute (route: IRoute): route is IResolverRoute {
return "resolve" in route;
export function isResolverRoute(route: IRoute): route is IResolverRoute {
return 'resolve' in route;
}
/**
* Traverses the router tree up to the root route.
* @param slot
*/
export function traverseRouterTree (slot: IRouterSlot): {tree: RouterTree, depth: number} {
export function traverseRouterTree(slot: IRouterSlot): { tree: RouterTree; depth: number } {
// Find the nodes from the route up to the root route
let routes: IRouterSlot[] = [slot];
while (slot.parent != null) {
@@ -174,12 +191,12 @@ export function traverseRouterTree (slot: IRouterSlot): {tree: RouterTree, depth
// Create the tree
const tree: RouterTree = routes.reduce((acc: RouterTree, slot: IRouterSlot) => {
return {slot, child: acc};
return { slot, child: acc };
}, undefined);
const depth = routes.length;
return {tree, depth};
return { tree, depth };
}
/**
@@ -187,7 +204,7 @@ export function traverseRouterTree (slot: IRouterSlot): {tree: RouterTree, depth
* @param tree
* @param depth
*/
export function getFragments (tree: RouterTree, depth: number): PathFragment[] {
export function getFragments(tree: RouterTree, depth: number): PathFragment[] {
let child = tree;
const fragments: PathFragment[] = [];
@@ -209,27 +226,27 @@ export function getFragments (tree: RouterTree, depth: number): PathFragment[] {
* @param slot
* @param path
*/
export function constructAbsolutePath<D = any, P = any> (slot: IRouterSlot<D, P>,
path: string | PathFragment = ""): string {
export function constructAbsolutePath<D = any, P = any>(
slot: IRouterSlot<D, P>,
path: string | PathFragment = '',
): string {
// Grab the router tree
const {tree, depth} = traverseRouterTree(slot);
const { tree, depth } = traverseRouterTree(slot);
// If the path starts with "/" we treat it as an absolute path
// and therefore don't continue because it is already absolute.
if (!path.startsWith("/")) {
if (!path.startsWith('/')) {
let traverseDepth = 0;
// If the path starts with "./" we can remove that part
// because we know the path is relative to its route.
if (path.startsWith("./")) {
if (path.startsWith('./')) {
path = path.slice(2);
}
// Match with the traverse flag.
const match = path.match(new RegExp(TRAVERSE_FLAG, "g"));
const match = path.match(new RegExp(TRAVERSE_FLAG, 'g'));
if (match != null) {
// If the path matched with the traverse flag we know that we have to construct
// a route until a certain depth. The traverse depth is the amount of "../" in the path
// and the depth is the part of the path we a slicing away.
@@ -243,12 +260,12 @@ export function constructAbsolutePath<D = any, P = any> (slot: IRouterSlot<D, P>
// Grab the fragments and construct the new path, taking the traverse depth into account.
// Always subtract at least 1 because we the path is relative to its parent.
// Filter away the empty fragments from the path.
const fragments = getFragments(tree, depth - 1 - traverseDepth).filter(fragment => fragment.length > 0);
path = `${fragments.join("/")}${fragments.length > 0 ? "/" : ""}${path}`;
const fragments = getFragments(tree, depth - 1 - traverseDepth).filter((fragment) => fragment.length > 0);
path = `${fragments.join('/')}${fragments.length > 0 ? '/' : ''}${path}`;
}
// Add the base path in front of the path. If the path is already absolute, the path wont get the base path added.
return constructPathWithBasePath(path, {end: false});
return constructPathWithBasePath(path, { end: false });
}
/**
@@ -256,8 +273,12 @@ export function constructAbsolutePath<D = any, P = any> (slot: IRouterSlot<D, P>
* @param slot
* @param route
*/
export function handleRedirect (slot: IRouterSlot, route: IRedirectRoute) {
history.replaceState(history.state, "", `${constructAbsolutePath(slot, route.redirectTo)}${route.preserveQuery ? queryString() : ""}`);
export function handleRedirect(slot: IRouterSlot, route: IRedirectRoute) {
history.replaceState(
history.state,
'',
`${constructAbsolutePath(slot, route.redirectTo)}${route.preserveQuery ? queryString() : ''}`,
);
}
/**
@@ -265,20 +286,19 @@ export function handleRedirect (slot: IRouterSlot, route: IRedirectRoute) {
* @param currentMatch
* @param newMatch
*/
export function shouldNavigate<D> (currentMatch: IRouteMatch<D> | null, newMatch: IRouteMatch<D>) {
export function shouldNavigate<D>(currentMatch: IRouteMatch<D> | null, newMatch: IRouteMatch<D>) {
// If the current match is not defined we should always route.
if (currentMatch == null) {
return true;
}
// Extract information about the matches
const {route: currentRoute, fragments: currentFragments} = currentMatch;
const {route: newRoute, fragments: newFragments} = newMatch;
const { route: currentRoute, fragments: currentFragments } = currentMatch;
const { route: newRoute, fragments: newFragments } = newMatch;
const isSameRoute = currentRoute == newRoute;
const isSameFragments = currentFragments.consumed == newFragments.consumed;
// Only navigate if the URL consumption is new or if the two routes are no longer the same.
return !isSameFragments || !isSameRoute;
}
}

View File

@@ -1,11 +1,11 @@
import { ROUTER_SLOT_TAG_NAME } from "../config";
import { IRouterSlot } from "../model";
import { ROUTER_SLOT_TAG_NAME } from '../config';
import { IRouterSlot } from '../model';
/**
* Queries the parent router.
* @param $elem
*/
export function queryParentRouterSlot<D = any> ($elem: Element): IRouterSlot<D> | null {
export function queryParentRouterSlot<D = any>($elem: Element): IRouterSlot<D> | null {
return queryParentRoots<IRouterSlot<D>>($elem, ROUTER_SLOT_TAG_NAME);
}
@@ -17,14 +17,12 @@ export function queryParentRouterSlot<D = any> ($elem: Element): IRouterSlot<D>
* @param minRoots
* @param roots
*/
export function queryParentRoots<T> ($elem: Element, query: string, minRoots: number = 0, roots: number = 0): T | null {
export function queryParentRoots<T>($elem: Element, query: string, minRoots: number = 0, roots: number = 0): T | null {
// Grab the rood node and query it
const $root = (<any>$elem).getRootNode();
// If we are at the right level or above we can query!
if (roots >= minRoots) {
// See if there's a match
const match = $root.querySelector(query);
if (match != null && match != $elem) {

View File

@@ -1,13 +1,13 @@
import { ISlashOptions, Params, Query } from "../model";
import { ISlashOptions, Params, Query } from '../model';
const $anchor = document.createElement("a");
const $anchor = document.createElement('a');
/**
* The current path of the location.
* As default slashes are included at the start and end.
* @param options
*/
export function path (options: Partial<ISlashOptions> = {}): string {
export function path(options: Partial<ISlashOptions> = {}): string {
return slashify(window.location.pathname, options);
}
@@ -15,7 +15,7 @@ export function path (options: Partial<ISlashOptions> = {}): string {
* Returns the path without the base path.
* @param options
*/
export function pathWithoutBasePath (options: Partial<ISlashOptions> = {}): string {
export function pathWithoutBasePath(options: Partial<ISlashOptions> = {}): string {
return slashify(stripStart(path(), basePath()), options);
}
@@ -30,8 +30,8 @@ export function pathWithoutBasePath (options: Partial<ISlashOptions> = {}): stri
* To make this method more performant we could cache the anchor element.
* As default it will return the base path with slashes in front and at the end.
*/
export function basePath (options: Partial<ISlashOptions> = {}): string {
return constructPathWithBasePath(".", options);
export function basePath(options: Partial<ISlashOptions> = {}): string {
return constructPathWithBasePath('.', options);
}
/**
@@ -44,7 +44,7 @@ export function basePath (options: Partial<ISlashOptions> = {}): string {
* @param path
* @param options
*/
export function constructPathWithBasePath (path: string, options: Partial<ISlashOptions> = {}) {
export function constructPathWithBasePath(path: string, options: Partial<ISlashOptions> = {}) {
$anchor.href = path;
return slashify($anchor.pathname, options);
}
@@ -54,14 +54,14 @@ export function constructPathWithBasePath (path: string, options: Partial<ISlash
* @param path
* @param part
*/
export function stripStart (path: string, part: string) {
return path.replace(new RegExp(`^${part}`), "");
export function stripStart(path: string, part: string) {
return path.replace(new RegExp(`^${part}`), '');
}
/**
* Returns the query string.
*/
export function queryString (): string {
export function queryString(): string {
return window.location.search;
}
@@ -69,7 +69,7 @@ export function queryString (): string {
* Returns the params for the current path.
* @returns Params
*/
export function query (): Query {
export function query(): Query {
return toQuery(queryString().substr(1));
}
@@ -77,16 +77,16 @@ export function query (): Query {
* Strips the slash from the start and end of a path.
* @param path
*/
export function stripSlash (path: string): string {
return slashify(path, {start: false, end: false});
export function stripSlash(path: string): string {
return slashify(path, { start: false, end: false });
}
/**
* Ensures the path starts and ends with a slash
* @param path
*/
export function ensureSlash (path: string): string {
return slashify(path, {start: true, end: true});
export function ensureSlash(path: string): string {
return slashify(path, { start: true, end: true });
}
/**
@@ -95,9 +95,9 @@ export function ensureSlash (path: string): string {
* @param start
* @param end
*/
export function slashify (path: string, {start = true, end = true}: Partial<ISlashOptions> = {}): string {
path = start && !path.startsWith("/") ? `/${path}` : (!start && path.startsWith("/") ? path.slice(1) : path);
return end && !path.endsWith("/") ? `${path}/` : (!end && path.endsWith("/") ? path.slice(0, path.length - 1) : path);
export function slashify(path: string, { start = true, end = true }: Partial<ISlashOptions> = {}): string {
path = start && !path.startsWith('/') ? `/${path}` : !start && path.startsWith('/') ? path.slice(1) : path;
return end && !path.endsWith('/') ? `${path}/` : !end && path.endsWith('/') ? path.slice(0, path.length - 1) : path;
}
/**
@@ -105,31 +105,33 @@ export function slashify (path: string, {start = true, end = true}: Partial<ISla
* @param {string} queryString (example: ("test=123&hejsa=LOL&wuhuu"))
* @returns {Query}
*/
export function toQuery (queryString: string): Query {
export function toQuery(queryString: string): Query {
// If the query does not contain anything, return an empty object.
if (queryString.length === 0) {
return {};
}
// Grab the atoms (["test=123", "hejsa=LOL", "wuhuu"])
const atoms = queryString.split("&");
const atoms = queryString.split('&');
// Split by the values ([["test", "123"], ["hejsa", "LOL"], ["wuhuu"]])
const arrayMap = atoms.map(atom => atom.split("="));
const arrayMap = atoms.map((atom) => atom.split('='));
// Assign the values to an object ({ test: "123", hejsa: "LOL", wuhuu: "" })
return Object.assign({}, ...arrayMap.map(arr => ({
[decodeURIComponent(arr[0])]: (arr.length > 1 ? decodeURIComponent(arr[1]) : "")
})));
return Object.assign(
{},
...arrayMap.map((arr) => ({
[decodeURIComponent(arr[0])]: arr.length > 1 ? decodeURIComponent(arr[1]) : '',
})),
);
}
/**
* Turns a query object into a string query.
* @param query
*/
export function toQueryString (query: Query): string {
export function toQueryString(query: Query): string {
return Object.entries(query)
.map(([key, value]) => `${key}${value != "" ? `=${encodeURIComponent(value)}` : ""}`)
.join("&");
.map(([key, value]) => `${key}${value != '' ? `=${encodeURIComponent(value)}` : ''}`)
.join('&');
}

View File

@@ -3,19 +3,22 @@ import { UmbContextConsumer } from './context-consumer.js';
import { UmbContextCallback } from './context-request.event.js';
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
export class UmbContextConsumerController<
BaseType = unknown,
ResultType extends BaseType = BaseType
> extends UmbContextConsumer<BaseType, ResultType> implements UmbController {
export class UmbContextConsumerController<BaseType = unknown, ResultType extends BaseType = BaseType>
extends UmbContextConsumer<BaseType, ResultType>
implements UmbController
{
#controllerAlias = Symbol();
#host: UmbControllerHost;
public get controllerAlias() {
return this.#controllerAlias;
}
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<BaseType, ResultType>, callback: UmbContextCallback<ResultType>) {
constructor(
host: UmbControllerHost,
contextAlias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
) {
super(host.getHostElement(), contextAlias, callback);
this.#host = host;
host.addController(this);

View File

@@ -46,7 +46,7 @@ describe('UmbContextProviderController', () => {
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
}
},
);
localConsumer.hostConnected();
});

View File

@@ -3,18 +3,25 @@ import { UmbContextProvider } from './context-provider.js';
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
export class UmbContextProviderController<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType
> extends UmbContextProvider<BaseType, ResultType> implements UmbController {
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType,
>
extends UmbContextProvider<BaseType, ResultType>
implements UmbController
{
#host: UmbControllerHost;
#controllerAlias:string;
#controllerAlias: string;
public get controllerAlias() {
return this.#controllerAlias;
}
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<BaseType, ResultType>, instance: InstanceType) {
constructor(
host: UmbControllerHost,
contextAlias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
) {
super(host.getHostElement(), contextAlias, instance);
this.#host = host;
// Makes the controllerAlias unique for this instance, this enables multiple Contexts to be provided under the same name. (This only makes sense cause of Context Token Discriminators)
@@ -30,7 +37,9 @@ export class UmbContextProviderController<
// This just an additional awareness feature to make devs Aware, the alternative would be adding it anyway, but that would destroy existing controller of this alias.
// Back out, this instance is already provided, by another controller.
throw new Error(
`Context API: The context of '${this.controllerAlias}' and instance '${(instance as any).constructor?.name ?? 'unnamed'}' is already provided by another Context Provider Controller.`
`Context API: The context of '${this.controllerAlias}' and instance '${
(instance as any).constructor?.name ?? 'unnamed'
}' is already provided by another Context Provider Controller.`,
);
} else {
host.addController(this);

View File

@@ -25,7 +25,7 @@ describe('UmbContextProvider', () => {
element = await fixture(
html` <umb-context-provider key="test-context" .value=${contextValue}>
<umb-test-context></umb-test-context>
</umb-context-provider>`
</umb-context-provider>`,
);
consumer = element.getElementsByTagName('umb-test-context')[0] as unknown as UmbTestContextElement;
});

View File

@@ -22,16 +22,19 @@ export declare class UmbElement extends UmbControllerHostElement {
observe<T>(
source: Observable<T> | { asObservable: () => Observable<T> },
callback: ObserverCallback<T>,
unique?: string
unique?: string,
): UmbObserverController<T>;
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType
>(alias: string | UmbContextToken<BaseType, ResultType>, instance: InstanceType): UmbContextProviderController<BaseType, ResultType, InstanceType>;
InstanceType extends ResultType = ResultType,
>(
alias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
): UmbContextProviderController<BaseType, ResultType, InstanceType>;
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
/**
* Use the UmbLocalizeController to localize your element.
@@ -65,9 +68,11 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType
>
(alias: string | UmbContextToken<BaseType, ResultType>, instance: InstanceType): UmbContextProviderController<BaseType, ResultType, InstanceType> {
InstanceType extends ResultType = ResultType,
>(
alias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
): UmbContextProviderController<BaseType, ResultType, InstanceType> {
return new UmbContextProviderController(this, alias, instance);
}
@@ -80,7 +85,7 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
*/
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType> {
return new UmbContextConsumerController(this, alias, callback);
}

View File

@@ -2,5 +2,5 @@ import { UmbConditionConfigBase } from '../types/index.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export type UmbConditionControllerArguments<
ConditionConfigType extends UmbConditionConfigBase = UmbConditionConfigBase
ConditionConfigType extends UmbConditionConfigBase = UmbConditionConfigBase,
> = { host: UmbControllerHost; config: ConditionConfigType; onChange: () => void };

View File

@@ -48,7 +48,7 @@ describe('UmbExtensionElementController', () => {
extensionController.destroy();
}
}
}
},
);
});
@@ -75,7 +75,7 @@ describe('UmbExtensionElementController', () => {
}
}
},
'umb-test-fallback-element'
'umb-test-fallback-element',
);
});
});
@@ -143,7 +143,7 @@ describe('UmbExtensionElementController', () => {
done();
extensionController.destroy(); // need to destroy the controller.
}
}
},
);
});
});

View File

@@ -8,21 +8,21 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
* When the extension is permitted to be used, the manifest is available for the consumer.
*
* @example
* ```ts
* const controller = new UmbExtensionManifestController(host, extensionRegistry, alias, (permitted, ctrl) => { console.log("Extension is permitted and this is the manifest: ", ctrl.manifest) }));
* ```
* ```ts
* const controller = new UmbExtensionManifestController(host, extensionRegistry, alias, (permitted, ctrl) => { console.log("Extension is permitted and this is the manifest: ", ctrl.manifest) }));
* ```
* @export
* @class UmbExtensionManifestController
*/
export class UmbExtensionManifestInitializer<
ManifestType extends ManifestWithDynamicConditions = ManifestWithDynamicConditions,
ControllerType extends UmbBaseExtensionInitializer<ManifestType, any> = any
ControllerType extends UmbBaseExtensionInitializer<ManifestType, any> = any,
> extends UmbBaseExtensionInitializer<ManifestType, ControllerType> {
constructor(
host: UmbControllerHost,
extensionRegistry: UmbExtensionRegistry<ManifestCondition>,
alias: string,
onPermissionChanged: (isPermitted: boolean, controller: ControllerType) => void
onPermissionChanged: (isPermitted: boolean, controller: ControllerType) => void,
) {
super(host, extensionRegistry, 'extManifest_', alias, onPermissionChanged);
this._init();

View File

@@ -1,11 +1,10 @@
import { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
import { type PermittedControllerType, UmbBaseExtensionsInitializer } from './base-extensions-initializer.controller.js';
import { UmbExtensionApiInitializer } from './extension-api-initializer.controller.js';
import {
type UmbExtensionRegistry,
ManifestApi,
ManifestBase,
} from '@umbraco-cms/backoffice/extension-api';
type PermittedControllerType,
UmbBaseExtensionsInitializer,
} from './base-extensions-initializer.controller.js';
import { UmbExtensionApiInitializer } from './extension-api-initializer.controller.js';
import { type UmbExtensionRegistry, ManifestApi, ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
@@ -26,7 +25,7 @@ export class UmbExtensionsApiInitializer<
ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ManifestTypeName>,
ManifestTypeAsApi extends ManifestApi = ManifestType extends ManifestApi ? ManifestType : never,
ControllerType extends UmbExtensionApiInitializer<ManifestTypeAsApi> = UmbExtensionApiInitializer<ManifestTypeAsApi>,
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>,
> extends UmbBaseExtensionsInitializer<
ManifestTypes,
ManifestTypeName,
@@ -59,7 +58,7 @@ export class UmbExtensionsApiInitializer<
type: ManifestTypeName | Array<ManifestTypeName>,
constructorArguments: Array<unknown> | undefined,
filter?: undefined | null | ((manifest: ManifestTypeAsApi) => boolean),
onChange?: (permittedManifests: Array<MyPermittedControllerType>) => void
onChange?: (permittedManifests: Array<MyPermittedControllerType>) => void,
) {
super(host, extensionRegistry, type, filter, onChange);
this.#extensionRegistry = extensionRegistry;
@@ -73,7 +72,7 @@ export class UmbExtensionsApiInitializer<
this.#extensionRegistry,
manifest.alias,
this.#constructorArgs,
this._extensionChanged
this._extensionChanged,
) as ControllerType;
return extController;

View File

@@ -1,10 +1,10 @@
import type { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
import { UmbExtensionManifestInitializer } from './extension-manifest-initializer.controller.js';
import { type PermittedControllerType, UmbBaseExtensionsInitializer } from './base-extensions-initializer.controller.js';
import {
type ManifestBase,
type UmbExtensionRegistry,
} from '@umbraco-cms/backoffice/extension-api';
type PermittedControllerType,
UmbBaseExtensionsInitializer,
} from './base-extensions-initializer.controller.js';
import { type ManifestBase, type UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api';
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
@@ -14,7 +14,7 @@ export class UmbExtensionsManifestInitializer<
ManifestTypeName extends keyof ManifestTypeMap<ManifestTypes> | string,
ManifestType extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ManifestTypeName>,
ControllerType extends UmbExtensionManifestInitializer<ManifestType> = UmbExtensionManifestInitializer<ManifestType>,
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>
MyPermittedControllerType extends ControllerType = PermittedControllerType<ControllerType>,
> extends UmbBaseExtensionsInitializer<
ManifestTypes,
ManifestTypeName,
@@ -30,7 +30,7 @@ export class UmbExtensionsManifestInitializer<
extensionRegistry: UmbExtensionRegistry<ManifestTypes>,
type: ManifestTypeName | Array<ManifestTypeName>,
filter: null | ((manifest: ManifestType) => boolean),
onChange: (permittedManifests: Array<MyPermittedControllerType>) => void
onChange: (permittedManifests: Array<MyPermittedControllerType>) => void,
) {
super(host, extensionRegistry, type, filter, onChange);
this.#extensionRegistry = extensionRegistry;
@@ -42,7 +42,7 @@ export class UmbExtensionsManifestInitializer<
this,
this.#extensionRegistry,
manifest.alias,
this._extensionChanged
this._extensionChanged,
) as ControllerType;
}
}

View File

@@ -1,36 +1,38 @@
import { UmbApi } from "../models/api.interface.js";
import { ManifestApi, ManifestElementAndApi } from "../types/base.types.js";
import { loadManifestApi } from "./load-manifest-api.function.js";
import { UmbApi } from '../models/api.interface.js';
import { ManifestApi, ManifestElementAndApi } from '../types/base.types.js';
import { loadManifestApi } from './load-manifest-api.function.js';
export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(manifest: ManifestApi<ApiType> | ManifestElementAndApi<any, ApiType>, constructorArguments: Array<unknown> = []): Promise<ApiType | undefined> {
if(manifest.api) {
export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(
manifest: ManifestApi<ApiType> | ManifestElementAndApi<any, ApiType>,
constructorArguments: Array<unknown> = [],
): Promise<ApiType | undefined> {
if (manifest.api) {
const apiConstructor = await loadManifestApi<ApiType>(manifest.api);
if(apiConstructor) {
if (apiConstructor) {
return new apiConstructor(...constructorArguments);
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'api', using either a 'api' or 'default' export`,
manifest
manifest,
);
}
}
if(manifest.js) {
if (manifest.js) {
const apiConstructor2 = await loadManifestApi<ApiType>(manifest.js);
if(apiConstructor2) {
if (apiConstructor2) {
return new apiConstructor2(...constructorArguments);
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'js', using either a 'api' or 'default' export`,
manifest
manifest,
);
}
}
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a JavaScript file via the 'api' or 'js' property.`,
manifest
manifest,
);
return undefined;

View File

@@ -3,9 +3,6 @@ import { ManifestApi } from '../types/index.js';
import { UmbApi } from '../models/api.interface.js';
import { createExtensionApi } from './create-extension-api.function.js';
class UmbExtensionApiTrueTestClass implements UmbApi {
isValidClassInstance() {
return true;
@@ -20,33 +17,25 @@ class UmbExtensionApiFalseTestClass implements UmbApi {
destroy() {}
}
const jsModuleWithDefaultExport = {
default: UmbExtensionApiTrueTestClass
default: UmbExtensionApiTrueTestClass,
};
const jsModuleWithApiExport = {
api: UmbExtensionApiTrueTestClass
api: UmbExtensionApiTrueTestClass,
};
const jsModuleWithDefaultAndApiExport = {
default: UmbExtensionApiFalseTestClass,
api: UmbExtensionApiTrueTestClass
api: UmbExtensionApiTrueTestClass,
};
describe('Extension-Api: Create Extension Api', () => {
it('Returns `undefined` when manifest does not have any correct properties', async () => {
const manifest: ManifestApi = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name'
name: 'pretty name',
};
const api = await createExtensionApi(manifest, []);
@@ -54,70 +43,65 @@ describe('Extension-Api: Create Extension Api', () => {
});
it('Handles when `api` property contains a class constructor', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
api: UmbExtensionApiTrueTestClass
api: UmbExtensionApiTrueTestClass,
};
const api = await createExtensionApi(manifest, []);
expect(api).to.not.be.undefined;
if(api) {
if (api) {
expect(api.isValidClassInstance()).to.be.true;
}
});
it('Handles when `loader` has a default export', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultExport)
js: () => Promise.resolve(jsModuleWithDefaultExport),
};
const api = await createExtensionApi(manifest, []);
expect(api).to.not.be.undefined;
if(api) {
if (api) {
expect(api.isValidClassInstance()).to.be.true;
}
});
it('Handles when `loader` has a api export', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithApiExport)
js: () => Promise.resolve(jsModuleWithApiExport),
};
const api = await createExtensionApi(manifest, []);
expect(api).to.not.be.undefined;
if(api) {
if (api) {
expect(api.isValidClassInstance()).to.be.true;
}
});
it('Prioritizes api export from loader property', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultAndApiExport)
js: () => Promise.resolve(jsModuleWithDefaultAndApiExport),
};
const api = await createExtensionApi(manifest, []);
expect(api).to.not.be.undefined;
if(api) {
if (api) {
expect(api.isValidClassInstance()).to.be.true;
}
});
//TODO: Test apiJs
//TODO: Test js
});

View File

@@ -1,43 +1,45 @@
import { ManifestElement, ManifestElementAndApi } from "../types/base.types.js";
import { loadManifestElement } from "./load-manifest-element.function.js";
import { ManifestElement, ManifestElementAndApi } from '../types/base.types.js';
import { loadManifestElement } from './load-manifest-element.function.js';
export async function createExtensionElement<ElementType extends HTMLElement>(manifest: ManifestElement<ElementType> | ManifestElementAndApi<ElementType>, fallbackElement?: string): Promise<ElementType | undefined> {
if(manifest.element) {
export async function createExtensionElement<ElementType extends HTMLElement>(
manifest: ManifestElement<ElementType> | ManifestElementAndApi<ElementType>,
fallbackElement?: string,
): Promise<ElementType | undefined> {
if (manifest.element) {
const elementConstructor = await loadManifestElement<ElementType>(manifest.element);
if(elementConstructor) {
if (elementConstructor) {
return new elementConstructor();
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'element', using either a 'element' or 'default' export`,
manifest
manifest,
);
}
}
if(manifest.js) {
if (manifest.js) {
const elementConstructor2 = await loadManifestElement<ElementType>(manifest.js);
if(elementConstructor2) {
if (elementConstructor2) {
return new elementConstructor2();
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'js', using either a 'element' or 'default' export`,
manifest
manifest,
);
}
}
if(manifest.elementName) {
if (manifest.elementName) {
return document.createElement(manifest.elementName) as ElementType;
}
if(fallbackElement) {
if (fallbackElement) {
return document.createElement(fallbackElement) as ElementType;
}
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an element, missing a JavaScript file via the 'element' or 'js' property or a Element Name in 'elementName' in the manifest.`,
manifest
manifest,
);
return undefined;
}

View File

@@ -3,8 +3,6 @@ import { ManifestElement, ManifestElementAndApi } from '../types/index.js';
import { createExtensionElement } from './create-extension-element.function.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-extension-api-true-test-element')
class UmbExtensionApiTrueTestElement extends HTMLElement {
isValidClassInstance() {
@@ -19,32 +17,25 @@ class UmbExtensionApiFalseTestElement extends HTMLElement {
}
}
const jsModuleWithDefaultExport = {
default: UmbExtensionApiTrueTestElement
default: UmbExtensionApiTrueTestElement,
};
const jsModuleWithElementExport = {
element: UmbExtensionApiTrueTestElement
element: UmbExtensionApiTrueTestElement,
};
const jsModuleWithDefaultAndElementExport = {
default: UmbExtensionApiFalseTestElement,
element: UmbExtensionApiTrueTestElement
element: UmbExtensionApiTrueTestElement,
};
describe('Extension-Api: Create Extension Element', () => {
it('Returns `undefined` when manifest does not have any correct properties', async () => {
const manifest: ManifestElement = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name'
name: 'pretty name',
};
const api = await createExtensionElement(manifest);
@@ -52,102 +43,94 @@ describe('Extension-Api: Create Extension Element', () => {
});
it('Returns fallback element instance when manifest does not provide element', async () => {
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name'
name: 'pretty name',
};
const element = await createExtensionElement(manifest, 'umb-extension-api-true-test-element');
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
it('Still returns fallback element instance when manifest does not provide element and manifest has api', async () => {
const manifest: ManifestElementAndApi<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name',
api: class TestApi {}
api: class TestApi {},
};
const element = await createExtensionElement(manifest, 'umb-extension-api-true-test-element');
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
it('Handles when `api` property contains a class constructor', async () => {
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name',
elementName: 'umb-extension-api-true-test-element'
elementName: 'umb-extension-api-true-test-element',
};
const element = await createExtensionElement(manifest);
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
it('Handles when `loader` has a default export', async () => {
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultExport)
js: () => Promise.resolve(jsModuleWithDefaultExport),
};
const element = await createExtensionElement(manifest);
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
it('Handles when `loader` has a element export', async () => {
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithElementExport)
js: () => Promise.resolve(jsModuleWithElementExport),
};
const element = await createExtensionElement(manifest);
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
it('Prioritizes api export from loader property', async () => {
const manifest: ManifestElement<UmbExtensionApiTrueTestElement> = {
type: 'my-test-type',
alias: 'Umb.Test.CreateManifestElement',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultAndElementExport)
js: () => Promise.resolve(jsModuleWithDefaultAndElementExport),
};
const element = await createExtensionElement(manifest);
expect(element).to.not.be.undefined;
if(element) {
if (element) {
expect(element.isValidClassInstance()).to.be.true;
}
});
//TODO: Test elementJs
//TODO: Test js
});

View File

@@ -1,4 +1,3 @@
export * from './create-extension-api.function.js';
export * from './create-extension-element.function.js';
export * from './has-init-export.function.js';

View File

@@ -1,3 +1,3 @@
export interface UmbApi {
destroy?(): void;
}
}

View File

@@ -1,2 +1,2 @@
export * from './entry-point.interface.js'
export * from './api.interface.js'
export * from './entry-point.interface.js';
export * from './api.interface.js';

View File

@@ -2,4 +2,4 @@ export * from './has-api-export.function.js';
export * from './has-default-export.function.js';
export * from './has-element-export.function.js';
export * from './is-manifest-base-type.function.js';
export * from './is-manifest-element-name-type.function.js';
export * from './is-manifest-element-name-type.function.js';

View File

@@ -1,5 +1,5 @@
import type { ManifestBase } from "../types/manifest-base.interface.js";
import type { ManifestBase } from '../types/manifest-base.interface.js';
export function isManifestBaseType(x: unknown): x is ManifestBase {
return typeof x === 'object' && x !== null && 'alias' in x;
}
}

View File

@@ -1,4 +1,4 @@
import type { ManifestBase } from "./manifest-base.interface.js";
import type { ManifestBase } from './manifest-base.interface.js';
export interface UmbConditionConfigBase<AliasType extends string = string> {
alias: AliasType;
@@ -12,7 +12,7 @@ export type ConditionTypeMap<ConditionTypes extends UmbConditionConfigBase> = {
export type SpecificConditionTypeOrUmbConditionConfigBase<
ConditionTypes extends UmbConditionConfigBase,
T extends keyof ConditionTypeMap<ConditionTypes> | string
T extends keyof ConditionTypeMap<ConditionTypes> | string,
> = T extends keyof ConditionTypeMap<ConditionTypes> ? ConditionTypeMap<ConditionTypes>[T] : UmbConditionConfigBase;
export interface ManifestWithDynamicConditions<ConditionTypes extends UmbConditionConfigBase = UmbConditionConfigBase>
@@ -25,4 +25,4 @@ export interface ManifestWithDynamicConditions<ConditionTypes extends UmbConditi
* Define one or more extension aliases that this extension should overwrite.
*/
overwrites?: string | Array<string>;
}
}

View File

@@ -5,4 +5,4 @@ export * from './manifest-bundle.interface.js';
export * from './manifest-condition.interface.js';
export * from './manifest-entrypoint.interface.js';
export * from './manifest-kind.interface.js';
export * from './utils.js';
export * from './utils.js';

View File

@@ -1,4 +1,3 @@
export interface ManifestBase {
/**
* The type of extension such as dashboard etc...
@@ -26,4 +25,4 @@ export interface ManifestBase {
* Extensions such as dashboards are ordered by weight with lower numbers being first in the list
*/
weight?: number;
}
}

View File

@@ -1,5 +1,5 @@
import type { ManifestPlainJs } from "./base.types.js";
import type { ManifestBase } from "./manifest-base.interface.js";
import type { ManifestPlainJs } from './base.types.js';
import type { ManifestBase } from './manifest-base.interface.js';
/**
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.
@@ -8,4 +8,3 @@ export interface ManifestBundle<UmbManifestTypes extends ManifestBase = Manifest
extends ManifestPlainJs<{ [key: string]: Array<UmbManifestTypes> }> {
type: 'bundle';
}

View File

@@ -1,5 +1,5 @@
import type { UmbExtensionCondition } from "../condition/index.js";
import type { ManifestApi } from "./base.types.js";
import type { UmbExtensionCondition } from '../condition/index.js';
import type { ManifestApi } from './base.types.js';
/**
* This type of extension takes a JS module and registers all exported manifests from the pointed JS file.

View File

@@ -1,5 +1,5 @@
import type { UmbEntryPointModule } from "../models/index.js";
import type { ManifestPlainJs } from "./base.types.js";
import type { UmbEntryPointModule } from '../models/index.js';
import type { ManifestPlainJs } from './base.types.js';
/**
* This type of extension gives full control and will simply load the specified JS file

View File

@@ -1,4 +1,4 @@
import type { ManifestBase } from "./manifest-base.interface.js";
import type { ManifestBase } from './manifest-base.interface.js';
export type ManifestTypeMap<ManifestTypes extends ManifestBase> = {
[Manifest in ManifestTypes as Manifest['type']]: Manifest;
@@ -8,5 +8,5 @@ export type ManifestTypeMap<ManifestTypes extends ManifestBase> = {
export type SpecificManifestTypeOrManifestBase<
ManifestTypes extends ManifestBase,
T extends keyof ManifestTypeMap<ManifestTypes> | string
T extends keyof ManifestTypeMap<ManifestTypes> | string,
> = T extends keyof ManifestTypeMap<ManifestTypes> ? ManifestTypeMap<ManifestTypes>[T] : ManifestBase;

View File

@@ -6,14 +6,12 @@ import { BehaviorSubject } from '@umbraco-cms/backoffice/external/rxjs';
* @description - State ensures the data is unique, not updating any Observes unless there is an actual change of the value using `===`.
*/
export class UmbBasicState<T> {
protected _subject:BehaviorSubject<T>;
protected _subject: BehaviorSubject<T>;
constructor(initialData: T) {
this._subject = new BehaviorSubject(initialData);
}
/**
* @method asObservable
* @return {Observable} Observable that the State casts to.
@@ -22,7 +20,7 @@ export class UmbBasicState<T> {
* const myState = new UmbArrayState('Hello world');
*
* this.observe(myState, (latestStateValue) => console.log("Value is: ", latestStateValue));
*/
*/
public asObservable(): ReturnType<BehaviorSubject<T>['asObservable']> {
return this._subject.asObservable();
}
@@ -33,10 +31,10 @@ export class UmbBasicState<T> {
* @example <caption>Example retrieve the current data of a state</caption>
* const myState = new UmbArrayState('Hello world');
* console.log("Value is: ", myState.getValue());
*/
*/
public get value(): BehaviorSubject<T>['value'] {
return this._subject.value;
};
}
/**
* @method getValue
@@ -45,7 +43,7 @@ export class UmbBasicState<T> {
* @example <caption>Example retrieve the current data of a state</caption>
* const myState = new UmbArrayState('Hello world');
* console.log("Value is: ", myState.value);
*/
*/
public getValue(): ReturnType<BehaviorSubject<T>['getValue']> {
return this._subject.getValue();
}
@@ -53,7 +51,7 @@ export class UmbBasicState<T> {
/**
* @method destroy
* @description - Destroys this state and completes all observations made to it.
*/
*/
public destroy(): void {
this._subject.complete();
}
@@ -67,7 +65,7 @@ export class UmbBasicState<T> {
* // myState.value is equal 'Good morning'.
* myState.next('Goodnight')
* // myState.value is equal 'Goodnight'.
*/
*/
next(newData: T): void {
if (newData !== this._subject.getValue()) {
this._subject.next(newData);

View File

@@ -1,4 +1,4 @@
import { UmbBasicState } from "./basic-state.js";
import { UmbBasicState } from './basic-state.js';
export interface UmbClassStateData {
equal(otherClass: this | undefined): boolean;

View File

@@ -13,7 +13,7 @@
export function partialUpdateFrozenArray<T>(
data: T[],
partialEntry: Partial<T>,
findMethod: (entry: T) => boolean
findMethod: (entry: T) => boolean,
): T[] {
const unFrozenDataSet = [...data];
const indexToReplace = unFrozenDataSet.findIndex((x) => findMethod(x));

View File

@@ -695,7 +695,6 @@ class UmbDocumentData extends UmbEntityData<DocumentResponseModel> {
}
publish(id: string, data: PublishDocumentRequestModel) {
// Update detail data:
const foundIndex = this.data.findIndex((item) => item.id === id);
if (foundIndex !== -1) {
@@ -719,7 +718,6 @@ class UmbDocumentData extends UmbEntityData<DocumentResponseModel> {
}
unpublish(id: string, data: PublishDocumentRequestModel) {
// Update detail data:
const foundIndex = this.data.findIndex((item) => item.id === id);
if (foundIndex !== -1) {

View File

@@ -375,4 +375,4 @@ class UmbPartialViewsData extends UmbEntityData<PartialViewResponseModel> {
}
export const umbPartialViewSnippetsData = new UmbPartialViewSnippetsData();
export const umbPartialViewsData = new UmbPartialViewsData();
export const umbPartialViewsData = new UmbPartialViewsData();

View File

@@ -104,7 +104,7 @@ export const arrayFilter = (filterBy: Array<string>, value?: Array<string>): boo
}
return filterBy.some((filterValue: string) => value?.includes(filterValue));
}
};
export const stringFilter = (filterBy: Array<string>, value?: string): boolean => {
// if a filter is not set, return all items

View File

@@ -7,7 +7,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<ServertimeOffset>({ offset: -120 })
ctx.json<ServertimeOffset>({ offset: -120 }),
);
}),
];

View File

@@ -14,7 +14,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PagedIndexResponseModel>(PagedIndexers)
ctx.json<PagedIndexResponseModel>(PagedIndexers),
);
}),
@@ -51,7 +51,7 @@ export const handlers = [
ctx.json<PagedSearcherResponseModel>({
total: 0,
items: [{ name: 'ExternalSearcher' }, { name: 'InternalSearcher' }, { name: 'InternalMemberSearcher' }],
})
}),
);
}),
@@ -69,7 +69,7 @@ export const handlers = [
ctx.json<PagedSearchResultResponseModel>({
total: 0,
items: searchResultMockData,
})
}),
);
} else {
return res(ctx.status(404));

View File

@@ -22,7 +22,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PagedHealthCheckGroupResponseModel>({ total: 9999, items: healthGroupsWithoutResult })
ctx.json<PagedHealthCheckGroupResponseModel>({ total: 9999, items: healthGroupsWithoutResult }),
);
}),
@@ -74,7 +74,7 @@ export const handlers = [
// Respond with a 200 status code
ctx.delay(1000),
ctx.status(200),
ctx.json<HealthCheckResultResponseModel>(result)
ctx.json<HealthCheckResultResponseModel>(result),
);
}),
];

View File

@@ -74,7 +74,7 @@ export const handlers = [
requiresConnectionTest: true,
},
],
})
}),
);
}),
@@ -88,13 +88,13 @@ export const handlers = [
type: 'connection',
status: 400,
detail: 'Database connection failed',
})
}),
);
}
return res(
// Respond with a 200 status code
ctx.status(201)
ctx.status(201),
);
}),
@@ -113,12 +113,12 @@ export const handlers = [
errors: {
name: ['Database name is invalid'],
},
})
}),
);
}
return res(
// Respond with a 200 status code
ctx.status(201)
ctx.status(201),
);
}),
];

View File

@@ -55,7 +55,7 @@ export const handlers = [
errors: {
isoCode: ['Language with same iso code already exists'],
},
})
}),
);
}
}),

View File

@@ -75,7 +75,7 @@ export const manifestDevelopmentHandler = rest.get(umbracoPath('/package/manifes
},
],
},
])
]),
);
});
@@ -83,6 +83,6 @@ export const manifestEmptyHandler = rest.get(umbracoPath('/package/manifest'), (
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PackageManifestResponse>([])
ctx.json<PackageManifestResponse>([]),
);
});

View File

@@ -14,7 +14,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json({})
ctx.json({}),
);
}),
@@ -26,7 +26,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<OutOfDateStatusResponseModel>({})
ctx.json<OutOfDateStatusResponseModel>({}),
);
}),
];

View File

@@ -30,7 +30,7 @@ export const handlers = [
packageName: 'Package with a view',
},
],
})
}),
);
}),
@@ -47,7 +47,7 @@ export const handlers = [
ctx.json<PagedPackageDefinitionResponseModel>({
total: packageArray.length,
items: packageArray,
})
}),
);
}),

View File

@@ -8,14 +8,14 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<ProfilingStatusResponseModel>({ enabled: true })
ctx.json<ProfilingStatusResponseModel>({ enabled: true }),
);
}),
rest.put(umbracoPath('/profiling/status'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
ctx.status(200),
);
}),
];

View File

@@ -8,8 +8,8 @@ export const handlers = [
// Respond with a 200 status code
ctx.status(200),
ctx.json<string>(
'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.'
)
'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.',
),
);
}),
@@ -18,7 +18,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200)
ctx.status(200),
);
}),
@@ -27,14 +27,14 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(200)
ctx.status(200),
);
}),
rest.post(umbracoPath('/published-cache/collect'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200)
ctx.status(200),
);
}),
];

View File

@@ -12,7 +12,7 @@ export const serverRunningHandler = rest.get(umbracoPath('/server/status'), (_re
ctx.status(200),
ctx.json<ServerStatusResponseModel>({
serverStatus: RuntimeLevelModel.RUN,
})
}),
);
});
@@ -22,7 +22,7 @@ export const serverMustInstallHandler = rest.get(umbracoPath('/server/status'),
ctx.status(200),
ctx.json<ServerStatusResponseModel>({
serverStatus: RuntimeLevelModel.INSTALL,
})
}),
);
});
@@ -32,7 +32,7 @@ export const serverMustUpgradeHandler = rest.get(umbracoPath('/server/status'),
ctx.status(200),
ctx.json<ServerStatusResponseModel>({
serverStatus: RuntimeLevelModel.UPGRADE,
})
}),
);
});
@@ -42,6 +42,6 @@ export const serverVersionHandler = rest.get(umbracoPath('/server/version'), (_r
ctx.status(200),
ctx.json<VersionResponseModel>({
version: '13.0.0',
})
}),
);
});

View File

@@ -16,7 +16,7 @@ export const handlers = [
ctx.status(200),
ctx.json<TelemetryResponseModel>({
telemetryLevel,
})
}),
);
}),
@@ -31,7 +31,7 @@ export const handlers = [
{ telemetryLevel: TelemetryLevelModel.BASIC },
{ telemetryLevel: TelemetryLevelModel.DETAILED },
],
})
}),
);
}),
@@ -41,7 +41,7 @@ export const handlers = [
telemetryLevel = newLevel;
return res(
// Respond with a 200 status code
ctx.status(200)
ctx.status(200),
);
} else {
return res(ctx.status(400));

View File

@@ -14,7 +14,7 @@ export const handlers = [
oldVersion: '13.0.0',
newVersion: '13.1.0',
reportUrl: 'https://our.umbraco.com/download/releases/1000',
})
}),
);
}),
@@ -23,7 +23,7 @@ export const handlers = [
return res(
// Respond with a 200 status code
ctx.status(201)
ctx.status(201),
);
}),
];

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, CSSResultGroup, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import {
UmbNotificationHandler,
@@ -38,7 +38,7 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
? repeat(
this._notifications,
(notification: UmbNotificationHandler) => notification.key,
(notification) => html`${notification.element}`
(notification) => html`${notification.element}`,
)
: ''}
</uui-toast-notification-container>

View File

@@ -13,9 +13,10 @@ export default meta;
type Story = StoryObj<UmbBodyLayoutElement>;
export const Overview: Story = {
render: () => html` <umb-body-layout>
<div slot="header"><uui-button color="" look="placeholder">Header slot</uui-button></div>
<uui-button color="" look="placeholder">Main slot</uui-button>
<div slot="footer-info"><uui-button color="" look="placeholder">Footer slot</uui-button></div>
</umb-body-layout>`,
render: () =>
html` <umb-body-layout>
<div slot="header"><uui-button color="" look="placeholder">Header slot</uui-button></div>
<uui-button color="" look="placeholder">Main slot</uui-button>
<div slot="footer-info"><uui-button color="" look="placeholder">Footer slot</uui-button></div>
</umb-body-layout>`,
};

View File

@@ -8,8 +8,9 @@ export default {
id: 'umb-button-with-dropdown',
} as Meta;
export const AAAOverview: Story<UmbButtonWithDropdownElement> = () => html` <umb-button-with-dropdown>
Open me
<div slot="dropdown" style="background: pink; height: 300px">I am a dropdown</div>
</umb-button-with-dropdown>`;
export const AAAOverview: Story<UmbButtonWithDropdownElement> = () =>
html` <umb-button-with-dropdown>
Open me
<div slot="dropdown" style="background: pink; height: 300px">I am a dropdown</div>
</umb-button-with-dropdown>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-empty-state')

View File

@@ -6,9 +6,10 @@ import type { UmbEmptyStateElement } from './empty-state.element.js';
const meta: Meta<UmbEmptyStateElement> = {
title: 'Components/Empty State',
component: 'umb-empty-state',
render: (args) => html` <umb-empty-state .position=${args.position} .size=${args.size}
>There are no items to be found</umb-empty-state
>`,
render: (args) =>
html` <umb-empty-state .position=${args.position} .size=${args.size}
>There are no items to be found</umb-empty-state
>`,
};
export default meta;

View File

@@ -1,5 +1,5 @@
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/section';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -48,7 +48,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
(actions) => {
this._hasActions = actions.length > 0;
},
'umbEntityActionsObserver'
'umbEntityActionsObserver',
);
}

View File

@@ -92,7 +92,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
#props?: Record<string, unknown> = {};
@property({ type: String, attribute: 'default-element' })
public defaultElement?:string;
public defaultElement?: string;
@property()
public renderMethod?: (extension: UmbExtensionElementInitializer) => TemplateResult | HTMLElement | null | undefined;
@@ -114,7 +114,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
(extensionControllers) => {
this._permittedExts = extensionControllers;
},
this.defaultElement
this.defaultElement,
);
this.#extensionsController.properties = this.#props;
}
@@ -124,7 +124,7 @@ export class UmbExtensionSlotElement extends UmbLitElement {
return repeat(
this._permittedExts,
(ext) => ext.alias,
(ext) => (this.renderMethod ? this.renderMethod(ext) : ext.component)
(ext) => (this.renderMethod ? this.renderMethod(ext) : ext.component),
);
}

View File

@@ -76,7 +76,7 @@ describe('UmbExtensionSlotElement', () => {
element = await fixture(
html`<umb-extension-slot
type="dashboard"
.filter=${(x: ManifestDashboard) => x.alias === 'unit-test-ext-slot-element-manifest'}></umb-extension-slot>`
.filter=${(x: ManifestDashboard) => x.alias === 'unit-test-ext-slot-element-manifest'}></umb-extension-slot>`,
);
await sleep(0);
@@ -90,14 +90,14 @@ describe('UmbExtensionSlotElement', () => {
type="dashboard"
.filter=${(x: ManifestDashboard) => x.alias === 'unit-test-ext-slot-element-manifest'}
.renderMethod=${(controller: UmbExtensionElementInitializer) => html`<bla>${controller.component}</bla>`}>
</umb-extension-slot>`
</umb-extension-slot>`,
);
await sleep(0);
expect(element.shadowRoot!.firstElementChild?.nodeName).to.be.equal('BLA');
expect(element.shadowRoot!.firstElementChild?.firstElementChild).to.be.instanceOf(
UmbTestExtensionSlotManifestElement
UmbTestExtensionSlotManifestElement,
);
});
@@ -107,7 +107,7 @@ describe('UmbExtensionSlotElement', () => {
type="dashboard"
.filter=${(x: ManifestDashboard) => x.alias === 'unit-test-ext-slot-element-manifest'}
.props=${{ testProp: 'fooBar' }}>
</umb-extension-slot>`
</umb-extension-slot>`,
);
await sleep(0);

View File

@@ -1,5 +1,5 @@
import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
/**
* @element umb-footer-layout

View File

@@ -10,10 +10,11 @@ export default {
id: 'umb-footer-layout',
} as Meta;
export const AAAOverview: Story<UmbFooterLayoutElement> = () => html` <umb-body-layout>
<div slot="footer-info">
<uui-button color="" look="placeholder">Footer slot</uui-button
><uui-button color="" look="placeholder">Actions slot</uui-button>
</div>
</umb-body-layout>`;
export const AAAOverview: Story<UmbFooterLayoutElement> = () =>
html` <umb-body-layout>
<div slot="footer-info">
<uui-button color="" look="placeholder">Footer slot</uui-button
><uui-button color="" look="placeholder">Actions slot</uui-button>
</div>
</umb-body-layout>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, CSSResultGroup, html, LitElement, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import {
ManifestHeaderAppButtonKind,

View File

@@ -1,5 +1,5 @@
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-history-item')

View File

@@ -1,5 +1,5 @@
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-history-list')

View File

@@ -13,25 +13,25 @@ export default {
id: 'umb-history-list',
} as Meta;
export const AAAOverview: Story<UmbHistoryListElement> = () => html` <umb-history-list>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
</umb-history-list>`;
export const AAAOverview: Story<UmbHistoryListElement> = () =>
html` <umb-history-list>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>
</umb-history-list>`;
AAAOverview.storyName = 'Overview';
export const Node: Story<UmbHistoryItemElement> = () => html`<umb-history-item
name="Name attribute"
detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>`;
export const Node: Story<UmbHistoryItemElement> = () =>
html`<umb-history-item name="Name attribute" detail="Detail attribute">
Default slot
<uui-button slot="actions" label="action">Action slot</uui-button>
</umb-history-item>`;

View File

@@ -112,12 +112,12 @@ export class UmbInputMultiUrlElement extends FormControlMixin(UmbLitElement) {
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this.urls.length < this.min
() => !!this.min && this.urls.length < this.min,
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this.urls.length > this.max
() => !!this.max && this.urls.length > this.max,
);
this.myModalRegistration = new UmbModalRouteRegistrationController(this, UMB_LINK_PICKER_MODAL)

View File

@@ -102,7 +102,11 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
const manifests = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[];
for (const manifest of manifests) {
const plugin = manifest.js ? await loadManifestApi(manifest.js) : manifest.api ? await loadManifestApi(manifest.api) : undefined;
const plugin = manifest.js
? await loadManifestApi(manifest.js)
: manifest.api
? await loadManifestApi(manifest.api)
: undefined;
if (plugin) {
this.#plugins.push(plugin);
}

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
css,
html,
@@ -189,12 +189,13 @@ export class UmbTableElement extends LitElement {
return html` <uui-table-head-cell style="--uui-table-cell-padding: 0">
${when(
this.config.allowSelection,
() => html` <uui-checkbox
label="Select All"
style="padding: var(--uui-size-4) var(--uui-size-5);"
@change="${this._handleAllRowsCheckboxChange}"
?checked="${this.selection.length === this.items.length}">
</uui-checkbox>`
() =>
html` <uui-checkbox
label="Select All"
style="padding: var(--uui-size-4) var(--uui-size-5);"
@change="${this._handleAllRowsCheckboxChange}"
?checked="${this.selection.length === this.items.length}">
</uui-checkbox>`,
)}
</uui-table-head-cell>`;
}
@@ -217,12 +218,13 @@ export class UmbTableElement extends LitElement {
${when(!this.config.hideIcon, () => html`<uui-icon name=${ifDefined(item.icon)}></uui-icon>`)}
${when(
this.config.allowSelection,
() => html` <uui-checkbox
label="Select Row"
@click=${(e: PointerEvent) => e.stopPropagation()}
@change=${(event: Event) => this._handleRowCheckboxChange(event, item)}
?checked="${this._isSelected(item.id)}">
</uui-checkbox>`
() =>
html` <uui-checkbox
label="Select Row"
@click=${(e: PointerEvent) => e.stopPropagation()}
@change=${(event: Event) => this._handleRowCheckboxChange(event, item)}
?checked="${this._isSelected(item.id)}">
</uui-checkbox>`,
)}
</uui-table-cell>`;
}

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, LitElement, nothing, repeat, customElement, property } from '@umbraco-cms/backoffice/external/lit';
export interface TooltipMenuItem {
@@ -48,7 +48,7 @@ export class UmbTooltipMenuElement extends LitElement {
return repeat(
this.items,
(item) => item.label,
(item) => this._renderItem(item)
(item) => this._renderItem(item),
);
}

View File

@@ -13,7 +13,6 @@ import { DocumentVariantResponseModel, ContentStateModel } from '@umbraco-cms/ba
@customElement('umb-variant-selector')
export class UmbVariantSelectorElement extends UmbLitElement {
@state()
_variants: Array<DocumentVariantResponseModel> = [];
@@ -21,7 +20,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
@state()
_activeVariants: Array<ActiveVariant> = [];
@property({attribute: false})
@property({ attribute: false })
public get _activeVariantsCultures(): string[] {
return this._activeVariants.map((el) => el.culture ?? '') ?? [];
}
@@ -70,7 +69,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
this._variants = variants;
}
},
'_observeVariants'
'_observeVariants',
);
}
}
@@ -87,7 +86,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
this._activeVariants = activeVariants;
}
},
'_observeActiveVariants'
'_observeActiveVariants',
);
}
}
@@ -105,7 +104,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
(name) => {
this._name = name;
},
'_name'
'_name',
);
}
@@ -123,7 +122,11 @@ export class UmbVariantSelectorElement extends UmbLitElement {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string' && this.#variantContext && isNameablePropertySetContext(this.#variantContext)) {
if (
typeof target?.value === 'string' &&
this.#variantContext &&
isNameablePropertySetContext(this.#variantContext)
) {
this.#variantContext.setName(target.value);
}
}
@@ -196,33 +199,32 @@ export class UmbVariantSelectorElement extends UmbLitElement {
<uui-scroll-container>
<ul>
${this._variants.map(
(variant) =>
html`
<li class="${this._isVariantActive(variant.culture!) ? 'selected' : ''}">
<button
class="variant-selector-switch-button
(variant) => html`
<li class="${this._isVariantActive(variant.culture!) ? 'selected' : ''}">
<button
class="variant-selector-switch-button
${this._isNotPublishedMode(variant.culture!, variant.state!) ? 'add-mode' : ''}"
@click=${() => this._switchVariant(variant)}>
${this._isNotPublishedMode(variant.culture!, variant.state!)
? html`<uui-icon class="add-icon" name="icon-add"></uui-icon>`
: nothing}
<div>
${variant.name} <i>(${variant.culture})</i> ${variant.segment}
<div class="variant-selector-state">${variant.state}</div>
</div>
</button>
@click=${() => this._switchVariant(variant)}>
${this._isNotPublishedMode(variant.culture!, variant.state!)
? html`<uui-icon class="add-icon" name="icon-add"></uui-icon>`
: nothing}
<div>
${variant.name} <i>(${variant.culture})</i> ${variant.segment}
<div class="variant-selector-state">${variant.state}</div>
</div>
</button>
${this._isVariantActive(variant.culture!)
? nothing
: html`
<uui-button
class="variant-selector-split-view"
@click=${() => this._openSplitView(variant)}>
Split view
</uui-button>
`}
</li>
`
${this._isVariantActive(variant.culture!)
? nothing
: html`
<uui-button
class="variant-selector-split-view"
@click=${() => this._openSplitView(variant)}>
Split view
</uui-button>
`}
</li>
`,
)}
</ul>
</uui-scroll-container>
@@ -307,7 +309,12 @@ export class UmbVariantSelectorElement extends UmbLitElement {
font-size: 14px;
cursor: pointer;
border-bottom: 1px solid var(--uui-color-divider-standalone);
font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif;
font-family:
Lato,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
}
.variant-selector-switch-button:hover {

View File

@@ -11,6 +11,5 @@ export default meta;
type Story = StoryObj<UmbVariantSelectorElement>;
export const Overview: Story = {
args: {
},
args: {},
};

View File

@@ -9,5 +9,5 @@ export const UMB_DATA_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken<UmbDataTypeC
{
type: 'sidebar',
size: 'small',
}
},
);

View File

@@ -33,7 +33,7 @@ export class UmbDataTypeItemServerDataSource implements UmbItemDataSource<DataTy
this.#host,
DataTypeResource.getDataTypeItem({
id: ids,
})
}),
);
}
}

View File

@@ -9,8 +9,7 @@ import { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
*/
@customElement('umb-data-type-workspace-editor')
export class UmbDataTypeWorkspaceEditorElement extends UmbLitElement {
@property({attribute: false})
@property({ attribute: false })
manifest?: ManifestWorkspace;
@state()
@@ -43,7 +42,7 @@ export class UmbDataTypeWorkspaceEditorElement extends UmbLitElement {
}
this.removeControllerByAlias('_observeIsNew');
},
'_observeIsNew'
'_observeIsNew',
);
}

View File

@@ -1,4 +1,4 @@
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
css,
html,
@@ -60,7 +60,7 @@ export class UmbDebugElement extends UmbLitElement {
// Massage the data into a simplier array of objects
// From a function in the context-api '
this.contextData = contextData(contexts);
})
}),
);
}
@@ -118,7 +118,7 @@ export class UmbDebugElement extends UmbLitElement {
<ul>
${this._renderInstance(contextData.data)}
</ul>
</li>`
</li>`,
);
});
@@ -132,16 +132,14 @@ export class UmbDebugElement extends UmbLitElement {
return instanceTemplates.push(html`<li>Callable Function</li>`);
} else if (instance.type === 'object') {
if (instance.methods?.length) {
instanceTemplates.push(
html`
<li>
<strong>Methods</strong>
<ul>
${instance.methods?.map((methodName) => html`<li>${methodName}</li>`)}
</ul>
</li>
`
);
instanceTemplates.push(html`
<li>
<strong>Methods</strong>
<ul>
${instance.methods?.map((methodName) => html`<li>${methodName}</li>`)}
</ul>
</li>
`);
}
const props: TemplateResult[] = [];

View File

@@ -9,7 +9,7 @@ import {
import { UmbFolderRepository } from '@umbraco-cms/backoffice/repository';
export class UmbFolderUpdateEntityAction<
T extends UmbFolderRepository = UmbFolderRepository
T extends UmbFolderRepository = UmbFolderRepository,
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;

View File

@@ -2,7 +2,7 @@ import { UmbEntityActionBase } from '../../entity-action.js';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbSortChildrenOfEntityAction<
T extends { sortChildrenOf(): Promise<void> }
T extends { sortChildrenOf(): Promise<void> },
> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);

View File

@@ -6,7 +6,9 @@ import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbr
* An action to perform on multiple entities
* For example for content you may wish to move one or more documents in bulk
*/
export interface ManifestEntityBulkAction extends ManifestElementAndApi<HTMLElement, UmbEntityBulkActionBase>, ManifestWithDynamicConditions<ConditionTypes> {
export interface ManifestEntityBulkAction
extends ManifestElementAndApi<HTMLElement, UmbEntityBulkActionBase>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'entityBulkAction';
meta: MetaEntityBulkAction;
}

View File

@@ -1,7 +1,7 @@
import type { ManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localization-api';
export interface ManifestLocalization extends ManifestPlainJs<{default: UmbLocalizationDictionary}> {
export interface ManifestLocalization extends ManifestPlainJs<{ default: UmbLocalizationDictionary }> {
type: 'localization';
meta: MetaLocalization;
}

View File

@@ -1,6 +1,7 @@
import type { UmbModalExtensionElement } from '../interfaces/modal-extension-element.interface.js';
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestModal extends ManifestElement<UmbModalExtensionElement<any, any> | UmbModalExtensionElement<any, undefined>> {
export interface ManifestModal
extends ManifestElement<UmbModalExtensionElement<any, any> | UmbModalExtensionElement<any, undefined>> {
type: 'modal';
}

View File

@@ -1,7 +1,9 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestPropertyAction extends ManifestElement<HTMLElement>, ManifestWithDynamicConditions<ConditionTypes> {
export interface ManifestPropertyAction
extends ManifestElement<HTMLElement>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'propertyAction';
meta: MetaPropertyAction;
}

View File

@@ -1,6 +1,8 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { UmbApi, ManifestApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
// TODO: Consider adding a ClassType for this manifest. (Currently we cannot know the scope of a repository, therefor we are going with ExtensionApi for now.)
export interface ManifestRepository<ApiType extends UmbApi = UmbApi> extends ManifestApi<ApiType>, ManifestWithDynamicConditions<ConditionTypes> {
export interface ManifestRepository<ApiType extends UmbApi = UmbApi>
extends ManifestApi<ApiType>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'repository';
}

View File

@@ -1,12 +1,11 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { InterfaceColor, InterfaceLook } from '@umbraco-cms/backoffice/external/uui';
import type {
ManifestElementAndApi,
ManifestWithDynamicConditions,
} from '@umbraco-cms/backoffice/extension-api';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
export interface ManifestWorkspaceAction extends ManifestElementAndApi<HTMLElement, UmbWorkspaceAction>, ManifestWithDynamicConditions<ConditionTypes> {
export interface ManifestWorkspaceAction
extends ManifestElementAndApi<HTMLElement, UmbWorkspaceAction>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'workspaceAction';
meta: MetaWorkspaceAction;
}

View File

@@ -16,23 +16,21 @@ export class UmbLocalizeDateElement extends UmbLitElement {
@property()
date!: string | Date;
/**
/**
* Formatting options
* @attr
* @example options={ dateStyle: 'full', timeStyle: 'long', timeZone: 'Australia/Sydney' }
*/
@property()
@property()
options?: Intl.DateTimeFormatOptions;
@state()
protected get text(): string {
return this.localize.date(this.date, this.options);
return this.localize.date(this.date, this.options);
}
protected render() {
return this.date
? html`${unsafeHTML(this.text)}`
: html`<slot></slot>`
return this.date ? html`${unsafeHTML(this.text)}` : html`<slot></slot>`;
}
static styles = [

View File

@@ -16,23 +16,21 @@ export class UmbLocalizeNumberElement extends UmbLitElement {
@property()
number!: number | string;
/**
/**
* Formatting options
* @attr
* @example options={ style: 'currency', currency: 'EUR' }
*/
@property()
@property()
options?: Intl.NumberFormatOptions;
@state()
protected get text(): string {
return this.localize.number(this.number, this.options);
return this.localize.number(this.number, this.options);
}
protected render() {
return this.number
? html`${unsafeHTML(this.text)}`
: html`<slot></slot>`
return this.number ? html`${unsafeHTML(this.text)}` : html`<slot></slot>`;
}
static styles = [

View File

@@ -16,15 +16,15 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement {
@property()
time!: number;
/**
/**
* Formatting options
* @attr
* @example options={ dateStyle: 'full', timeStyle: 'long', timeZone: 'Australia/Sydney' }
*/
@property()
@property()
options?: Intl.RelativeTimeFormatOptions;
/**
/**
* Unit
* @attr
* @example unit='seconds'
@@ -34,13 +34,11 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement {
@state()
protected get text(): string {
return this.localize.relativeTime(this.time, this.unit, this.options);
return this.localize.relativeTime(this.time, this.unit, this.options);
}
protected render() {
return this.time
? html`${unsafeHTML(this.text)}`
: html`<slot></slot>`
return this.time ? html`${unsafeHTML(this.text)}` : html`<slot></slot>`;
}
static styles = [

View File

@@ -9,7 +9,7 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-localize')
export class UmbLocalizeElement extends UmbLitElement {
/**
* The key to localize. The key is case sensitive.
* The key to localize. The key is case sensitive.
* @attr
* @example key="general_ok"
*/
@@ -33,7 +33,7 @@ export class UmbLocalizeElement extends UmbLitElement {
*/
@property()
debug = false;
@state()
protected get text(): string {
const localizedValue = this.localize.term(this.key, ...(this.args ?? []));
@@ -53,8 +53,8 @@ export class UmbLocalizeElement extends UmbLitElement {
return this.text.trim()
? html`${unsafeHTML(this.text)}`
: this.debug
? html`<span style="color:red">${this.key}</span>`
: html`<slot></slot>`;
? html`<span style="color:red">${this.key}</span>`
: html`<slot></slot>`;
}
static styles = [

View File

@@ -64,7 +64,7 @@ export class UmbLocalizationRegistry {
}
// If extension contains a js file, load it and add the default dictionary to the inner dictionary.
if(extension.js) {
if (extension.js) {
const loadedExtension = await loadManifestPlainJs(extension.js);
if (loadedExtension && hasDefaultExport<UmbLocalizationDictionary>(loadedExtension)) {

View File

@@ -5,7 +5,6 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-confirm-modal')
export class UmbConfirmModalElement extends UmbLitElement {
@property({ attribute: false })
modalContext?: UmbModalContext<UmbConfirmModalData, UmbConfirmModalValue>;

View File

@@ -10,8 +10,6 @@ import { UmbIconPickerModalData, UmbIconPickerModalValue, UmbModalBaseElement }
// TODO: to prevent element extension we need to move the Picker logic into a separate class we can reuse across all pickers
@customElement('umb-icon-picker-modal')
export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPickerModalData, UmbIconPickerModalValue> {
private _iconList = icons.filter((icon) => !icon.legacy);
@state()
@@ -19,26 +17,26 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
@state()
private _colorList = [
{alias:'text', varName:'--uui-color-text'},
{alias:'yellow', varName:'--uui-palette-sunglow'},
{alias:'pink', varName:'--uui-palette-spanish-pink'},
{alias:'dark', varName:'--uui-palette-gunmetal'},
{alias:'darkblue', varName:'--uui-palette-space-cadet'},
{alias:'blue', varName:'--uui-palette-violet-blue'},
{alias:'red', varName:'--uui-palette-maroon-flush'},
{alias:'green', varName:'--uui-palette-jungle-green'},
{alias:'brown', varName:'--uui-palette-chamoisee'},
{ alias: 'text', varName: '--uui-color-text' },
{ alias: 'yellow', varName: '--uui-palette-sunglow' },
{ alias: 'pink', varName: '--uui-palette-spanish-pink' },
{ alias: 'dark', varName: '--uui-palette-gunmetal' },
{ alias: 'darkblue', varName: '--uui-palette-space-cadet' },
{ alias: 'blue', varName: '--uui-palette-violet-blue' },
{ alias: 'red', varName: '--uui-palette-maroon-flush' },
{ alias: 'green', varName: '--uui-palette-jungle-green' },
{ alias: 'brown', varName: '--uui-palette-chamoisee' },
];
@state()
_modalValue?:UmbIconPickerModalValue;
_modalValue?: UmbIconPickerModalValue;
@state()
_currentColorVarName = '--uui-color-text';
#changeIcon(e: { target: HTMLInputElement; type: any; key: unknown }) {
if (e.type == 'click' || (e.type == 'keyup' && e.key == 'Enter')) {
this.modalContext?.updateValue({icon: e.target.id});
this.modalContext?.updateValue({ icon: e.target.id });
}
}
@@ -60,18 +58,23 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
}
#onColorChange(e: UUIColorSwatchesEvent) {
this.modalContext?.updateValue({icon: e.target.value});
this.modalContext?.updateValue({ icon: e.target.value });
}
connectedCallback() {
super.connectedCallback();
this._iconListFiltered = this._iconList;
if(this.modalContext) {
this.observe(this.modalContext?.value, (newValue) => {
this._modalValue = newValue;
this._currentColorVarName = this._colorList.find(x => x.alias === newValue?.color)?.alias ?? this._colorList[0].varName;
}, '_observeModalContextValue');
if (this.modalContext) {
this.observe(
this.modalContext?.value,
(newValue) => {
this._modalValue = newValue;
this._currentColorVarName =
this._colorList.find((x) => x.alias === newValue?.color)?.alias ?? this._colorList[0].varName;
},
'_observeModalContextValue',
);
}
}
@@ -88,10 +91,15 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
${
// TODO: Missing translation for the color aliases.
this._colorList.map(
(color) => html`
<uui-color-swatch label="${color.alias}" title="${color.alias}" value=${color.alias} style="--uui-swatch-color: var(${color.varName})"></uui-color-swatch>
`,
)}
(color) => html`
<uui-color-swatch
label="${color.alias}"
title="${color.alias}"
value=${color.alias}
style="--uui-swatch-color: var(${color.varName})"></uui-color-swatch>
`,
)
}
</uui-color-swatches>
<hr />
<uui-scroll-container id="icon-selection">${this.renderIconSelection()}</uui-scroll-container>
@@ -116,7 +124,8 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
}
renderIconSelection() {
return repeat(this._iconListFiltered,
return repeat(
this._iconListFiltered,
(icon) => icon.name,
(icon) => html`
<uui-icon
@@ -130,7 +139,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
@click="${this.#changeIcon}"
@keyup="${this.#changeIcon}">
</uui-icon>
`
`,
);
}

View File

@@ -13,8 +13,7 @@ export default {
id: 'umb-icon-picker-modal',
} as Meta;
const data: UmbIconPickerModalData = {
};
const data: UmbIconPickerModalData = {};
const value: UmbIconPickerModalValue = {
color: undefined,

View File

@@ -38,7 +38,8 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
this.observe(
umbExtensionsRegistry.extensionsOfType('section'),
(sections: Array<ManifestSection>) => (this._sections = sections),
), 'umbSectionsObserver';
),
'umbSectionsObserver';
}
render() {

View File

@@ -104,7 +104,7 @@ export class UmbModalElement extends UmbLitElement {
async #createInnerElement(manifest: ManifestModal) {
// TODO: add inner fallback element if no extension element is found
const innerElement = (await createExtensionElement(manifest));
const innerElement = await createExtensionElement(manifest);
if (innerElement) {
innerElement.data = this.#modalContext!.data;

Some files were not shown because too many files have changed in this diff Show More