This commit is contained in:
JesmoDev
2022-05-25 13:43:11 +02:00
15 changed files with 559 additions and 304 deletions

View File

@@ -1,29 +1,38 @@
import '@umbraco-ui/uui';
import '@umbraco-ui/uui-css/dist/uui-css.css';
// TODO: lazy load these
import './installer/installer.element';
import './auth/login/login.element';
import './auth/auth-layout.element';
import './backoffice/backoffice.element';
import './installer/installer.element';
import './backoffice/node-editor.element';
import { UmbSectionContext } from './section.context';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { customElement } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { getInitStatus } from './api/fetcher';
import { UmbRoute, UmbRouter } from './core/router';
import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router';
import { UmbContextProviderMixin } from './core/context';
const routes: Array<UmbRoute> = [
{
path: '/login',
elementName: 'umb-login',
alias: 'login',
meta: { requiresAuth: false },
},
{
path: '/install',
elementName: 'umb-installer',
alias: 'install',
meta: { requiresAuth: false },
},
{
path: '/section/:section',
elementName: 'umb-backoffice',
alias: 'app',
meta: { requiresAuth: true },
},
];
@@ -39,54 +48,105 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
}
`;
@state()
_authorized = false;
private _isInstalled = false;
_router?: UmbRouter;
private _view?: HTMLElement;
private _router?: UmbRouter;
private _locationSubscription?: Subscription;
constructor() {
super();
this._authorized = sessionStorage.getItem('is-authenticated') === 'true';
this.addEventListener(umbRouterBeforeEnterEventType, this._onBeforeEnter);
}
connectedCallback(): void {
super.connectedCallback();
const { extensionRegistry } = window.Umbraco;
this.provideContext('umbExtensionRegistry', window.Umbraco.extensionRegistry);
this.provideContext('umbSectionContext', new UmbSectionContext(extensionRegistry));
}
private _onBeforeEnter = (event: Event) => {
if (!isUmbRouterBeforeEnterEvent(event)) return;
this._handleUnauthorizedNavigation(event);
}
private _handleUnauthorizedNavigation(event: UmbRouterBeforeEnterEvent) {
if (event.to.route.meta.requiresAuth && !this._isAuthorized()) {
event.preventDefault();
this._router?.push('/login');
}
}
private _isAuthorized(): boolean {
return sessionStorage.getItem('is-authenticated') === 'true';
}
protected async firstUpdated(): Promise<void> {
const outlet = this.shadowRoot?.getElementById('outlet');
if (!outlet) return;
this._router = new UmbRouter(this, outlet);
this._router = new UmbRouter(this);
this._router.setRoutes(routes);
// TODO: find a solution for magic strings
this.provideContext('umbRouter', this._router);
// TODO: this is a temporary routing solution for shell elements
try {
const { data } = await getInitStatus({});
if (!data.installed) {
this._isInstalled = data.installed;
if (!this._isInstalled) {
this._router.push('/install');
return;
}
if (!this._authorized) {
if (!this._isAuthorized() || window.location.pathname === '/install') {
this._router.push('/login');
} else {
this._router.push('/section/content');
const next = window.location.pathname === '/' ? '/section/Content' : window.location.pathname;
this._router.push(next);
}
this._useLocation();
} catch (error) {
console.log(error);
}
}
private _useLocation () {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location
.subscribe((location: UmbRouteLocation) => {
if (location.route.alias === 'login') {
this._renderView('umb-login');
return;
}
if (location.route.alias === 'install') {
this._renderView('umb-installer');
return;
}
this._renderView('umb-backoffice');
});
}
_renderView (view: string) {
if (this._view?.tagName === view.toUpperCase()) return;
this._view = document.createElement(view);
this.requestUpdate();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
}
render() {
return html`
<div id="outlet"></div>
`;
return html`${this._view}`;
}
}

View File

@@ -62,7 +62,7 @@ export class UmbLogin extends UmbContextConsumerMixin(LitElement) {
await postUserLogin({ username, password, persist });
this._loggingIn = false;
// TODO: how do we know where to go?
this._router?.push('/section/content');
this._router?.push('/section/Content');
} catch (error) {
console.log(error);
this._loggingIn = false;

View File

@@ -1,13 +1,14 @@
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { getUserSections } from '../api/fetcher';
import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { UmbSectionContext } from '../section.context';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from '../core/extension';
import { UmbRouter } from '../core/router';
// TODO: umb or not umb in file name?
@@ -81,9 +82,6 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
@state()
private _open = false;
@state()
private _availableSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
@state()
private _allowedSection: Array<string> = [];
@@ -97,11 +95,29 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
private _extraSections: Array<UmbExtensionManifest<UmbManifestSectionMeta>> = [];
@state()
private _activeSection = '';
private _currentSectionAlias = '';
private _router?: UmbRouter;
private _extensionRegistry?: UmbExtensionRegistry;
private _subscription?: Subscription;
private _sectionContext?: UmbSectionContext;
private _sectionSubscription?: Subscription;
private _currentSectionSubscription?: Subscription;
private _locationSubscription?: Subscription;
private _location? : UmbRouteLocation;
constructor () {
super();
this.consumeContext('umbRouter', (_instance: UmbRouter) => {
this._router = _instance;
this._useLocation();
});
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
this._sectionContext = _instance;
this._useCurrentSection();
this._useSections();
});
}
private _handleMore(e: MouseEvent) {
e.stopPropagation();
@@ -118,12 +134,14 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
// TODO: this could maybe be handled by an anchor tag
this._router?.push(`/section/${section.name}`);
this._activeSection = section.alias;
this._sectionContext?.setCurrent(section.alias);
}
private _handleLabelClick(e: PointerEvent) {
const label = (e.target as any).label;
this._activeSection = label;
// TODO: set current section
//this._sectionContext?.setCurrent(section.alias);
const moreTab = this.shadowRoot?.getElementById('moreTab');
moreTab?.setAttribute('active', 'true');
@@ -131,38 +149,48 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
this._open = false;
}
connectedCallback() {
super.connectedCallback();
this.consumeContext('umbExtensionRegistry', (api: unknown) => {
this._extensionRegistry = api as UmbExtensionRegistry;
this._useSections();
private _useLocation () {
this._locationSubscription?.unsubscribe();
this._locationSubscription = this._router?.location
.subscribe((location: UmbRouteLocation) => {
this._location = location;
});
this.consumeContext('UmbRouter', (api: unknown) => {
this._router = api as UmbRouter;
}
private _useCurrentSection () {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent()
.subscribe(section => {
this._currentSectionAlias = section.alias;
});
}
private async _useSections() {
this._subscription?.unsubscribe();
this._sectionSubscription?.unsubscribe();
const { data } = await getUserSections({});
this._allowedSection = data.sections;
this._subscription = this._extensionRegistry?.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest<unknown>>) =>
extensions.filter(extension => extension.type === 'section')
))
this._sectionSubscription = this._sectionContext?.getSections()
.subscribe((sectionExtensions: any) => {
this._availableSections = [...sectionExtensions];
this._sections = this._availableSections.filter((section) => this._allowedSection.includes(section.alias));
// TODO: implement resize observer
this._sections = sectionExtensions.filter((section: any) => this._allowedSection.includes(section.alias));
this._visibleSections = this._sections;
this._activeSection = this._visibleSections?.[0].alias;
const currentSectionAlias = this._sections.find(section => section.name === this._location?.params?.section)?.alias;
if (!currentSectionAlias) return;
this._sectionContext?.setCurrent(currentSectionAlias);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._locationSubscription?.unsubscribe();
this._sectionSubscription?.unsubscribe();
this._currentSectionSubscription?.unsubscribe();
}
private _renderExtraSections() {
return when(
this._extraSections.length > 0,
@@ -177,7 +205,7 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
${this._extraSections.map(
(section) => html`
<uui-menu-item
?active="${this._activeSection === section.alias}"
?active="${this._currentSectionAlias === section.alias}"
label="${section.name}"
@click-label="${this._handleLabelClick}"></uui-menu-item>
`
@@ -201,7 +229,7 @@ export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) {
${this._visibleSections.map(
(section) => html`
<uui-tab
?active="${this._activeSection === section.alias}"
?active="${this._currentSectionAlias === section.alias}"
label="${section.name}"
@click="${(e: PointerEvent) => this._handleTabClick(e, section)}"></uui-tab>
`

View File

@@ -1,93 +1,76 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../core/context';
import { UmbSectionContext } from '../section.context';
import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension';
// TODO: lazy load these. How to we handle dynamic import of our typescript file?
import '../content/content-section.element';
import '../media/media-section.element';
@defineElement('umb-backoffice-main')
export class UmbBackofficeMain extends LitElement {
export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
:host {
flex: 1 1 auto;
}
// TODO: not call this editor, I would like to reserve that name for the view of editing data, like what goes in the router-outlet or in infinite editors (or inside Nested Content)
#editor {
background-color: var(--uui-color-background);
display: block;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#editor-top {
background-color: var(--uui-color-surface);
width: 100%;
display: flex;
flex: none;
gap: 16px;
align-items: center;
border-bottom: 1px solid var(--uui-color-border);
}
#editor-top uui-input {
width: 100%;
margin-left: 16px;
}
#editor-top uui-tab-group {
--uui-tab-divider: var(--uui-color-border);
border-left: 1px solid var(--uui-color-border);
flex-wrap: nowrap;
height: 60px;
}
#editor-content {
padding: var(--uui-size-6);
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
}
uui-tab {
font-size: 0.8rem;
}
#editor-bottom {
display: flex;
flex: none;
justify-content: end;
align-items: center;
height: 70px;
width: 100%;
gap: 16px;
padding-right: 24px;
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
`,
];
@state()
private _sectionElement?: HTMLElement;
private _sectionContext?: UmbSectionContext;
private _currentSectionSubscription?: Subscription;
constructor () {
super();
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
this._sectionContext = _instance;
this._useCurrentSection();
});
}
private _useCurrentSection () {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionContext?.getCurrent()
.subscribe(section => {
this._createSectionElement(section);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._currentSectionSubscription?.unsubscribe();
}
private async _createSectionElement (section: UmbExtensionManifest<UmbManifestSectionMeta>) {
if (!section) return;
// TODO: How do we handle dynamic imports of our files?
if (section.js) {
await import(/* @vite-ignore */section.js);
}
if (section.elementName) {
this._sectionElement = document.createElement(section.elementName);
}
}
render() {
return html`
<div id="editor">
<div id="editor-top">
<uui-input value="Home"></uui-input>
<uui-tab-group>
<uui-tab active>Content</uui-tab>
<uui-tab>Info</uui-tab>
<uui-tab disabled>Actions</uui-tab>
</uui-tab-group>
</div>
<uui-scroll-container id="editor-content"></uui-scroll-container>
<div id="editor-bottom">
<uui-button>Save and preview</uui-button>
<uui-button look="secondary">Save</uui-button>
<uui-button look="primary" color="positive">Save and publish</uui-button>
</div>
</div>
${ this._sectionElement }
`;
}
}

View File

@@ -35,10 +35,7 @@ export class UmbBackoffice extends LitElement {
render() {
return html`
<umb-backoffice-header></umb-backoffice-header>
<div id="main">
<umb-backoffice-sidebar></umb-backoffice-sidebar>
<umb-backoffice-main></umb-backoffice-main>
</div>
<umb-backoffice-main></umb-backoffice-main>
`;
}
}

View File

@@ -0,0 +1,100 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
@customElement('umb-node-editor')
class UmbNodeEditor extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
#node-editor {
background-color: var(--uui-color-background);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#node-editor-top {
background-color: var(--uui-color-surface);
width: 100%;
display: flex;
flex: none;
gap: 16px;
align-items: center;
border-bottom: 1px solid var(--uui-color-border);
}
#node-editor-top uui-input {
width: 100%;
margin-left: 16px;
}
#node-editor-top uui-tab-group {
--uui-tab-divider: var(--uui-color-border);
border-left: 1px solid var(--uui-color-border);
flex-wrap: nowrap;
height: 60px;
}
#node-editor-content {
padding: var(--uui-size-6);
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
}
uui-tab {
font-size: 0.8rem;
}
#node-editor-bottom {
display: flex;
flex: none;
justify-content: end;
align-items: center;
height: 70px;
width: 100%;
gap: 16px;
padding-right: 24px;
border-top: 1px solid var(--uui-color-border);
background-color: var(--uui-color-surface);
box-sizing: border-box;
}
`,
];
render () {
return html`
<div id="node-editor">
<div id="node-editor-top">
<uui-input value="Home"></uui-input>
<uui-tab-group>
<uui-tab active>Content</uui-tab>
<uui-tab>Info</uui-tab>
<uui-tab disabled>Actions</uui-tab>
</uui-tab-group>
</div>
<uui-scroll-container id="node-editor-content"></uui-scroll-container>
<div id="node-editor-bottom">
<uui-button>Save and preview</uui-button>
<uui-button look="secondary">Save</uui-button>
<uui-button look="primary" color="positive">Save and publish</uui-button>
</div>
</div>
`
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-node-editor': UmbNodeEditor;
}
}

View File

@@ -6,11 +6,17 @@ import { css, html, LitElement } from 'lit';
export class UmbContentSection extends LitElement {
static styles = [
UUITextStyles,
css``,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
render() {
return html`<div>Content Section</div>`;
return html`<umb-node-editor></umb-node-editor>`;
}
}

View File

@@ -3,7 +3,7 @@ import { UmbContextConsumer } from './context-consumer';
type Constructor<T = HTMLElement> = new (...args: any[]) => T;
export declare class UmbContextConsumerInterface {
consumeContext(alias: string, callback?: (_instance: unknown) => void):void;
consumeContext(alias: string, callback?: (_instance: any) => void):void;
whenAvailableOrChanged(contextAliases: string[], callback?: () => void):void;
}
@@ -68,13 +68,12 @@ export const UmbContextConsumerMixin = <T extends Constructor<HTMLElement>>(supe
this._resolved.clear();
}
_consumeContextCallback(newAlias, newInstance) {
_consumeContextCallback(_newAlias: string, _newInstance: unknown) {
// TODO: do be done.
}
// might return a object, so you can unsubscribe.
whenAvailableOrChanged(contextAliases: string[]) {
whenAvailableOrChanged(_contextAliases: string[]) {
// TODO: To be done.
}
};

View File

@@ -1,164 +1,2 @@
import { Observable, ReplaySubject } from 'rxjs';
export interface UmbRoute {
path: string;
elementName: string;
meta?: any;
}
export interface UmbRouteLocation {
pathname: string;
params: object;
fullPath: string;
route: UmbRoute;
}
export class UmbRouter {
private _routes: Array<UmbRoute> = [];
private _host: HTMLElement;
private _outlet: HTMLElement;
private _element: any;
private _location: ReplaySubject<UmbRouteLocation> = new ReplaySubject(1);
public readonly location: Observable<UmbRouteLocation> = this._location.asObservable();
constructor(host: HTMLElement, outlet: HTMLElement) {
this._host = host;
this._outlet = outlet;
// Anchor Hijacker
this._host.addEventListener('click', async (event: any) => {
const target = event.composedPath()[0];
const href = target.href;
if (!href) return;
event.preventDefault();
const url = new URL(href);
const pathname = url.pathname;
const canLeave = await this._requestLeave();
if (!canLeave) return;
this._navigate(pathname);
});
}
public setRoutes(routes: Array<UmbRoute>) {
this._routes = routes;
const pathname = window.location.pathname;
this.push(pathname);
}
public getRoutes() {
return this._routes;
}
public go(delta: number) {
history.go(delta);
}
public back() {
history.back();
}
public forward() {
history.forward();
}
public push(pathname: string) {
history.pushState(null, '', pathname);
this._navigate(pathname);
}
private async _requestLeave() {
if (this._element.beforeLeave) {
const res = await this._element.beforeLeave();
if (!res) return;
}
const beforeLeaveEvent = new CustomEvent('before-leave', {
bubbles: true,
composed: true,
cancelable: true,
});
this._host.dispatchEvent(beforeLeaveEvent);
if (beforeLeaveEvent.defaultPrevented) return;
return true;
}
private async _requestEnter(to: UmbRouteLocation) {
if (this._element.beforeEnter) {
const res = await this._element.beforeEnter();
if (!res) return;
}
const beforeEnterEvent = new CustomEvent('before-enter', {
bubbles: true,
composed: true,
cancelable: true,
detail: { to },
});
this._host.dispatchEvent(beforeEnterEvent);
if (beforeEnterEvent.defaultPrevented) return;
return true;
}
private async _navigate(pathname: string) {
const location = this._resolve(pathname);
if (!location) return;
this._setupElement(location);
const canEnter = await this._requestEnter(location);
if (!canEnter) return;
window.history.pushState(null, '', pathname);
this._location.next(location);
this._render();
}
private _resolve(pathname: string): UmbRouteLocation | null {
let location: UmbRouteLocation | null = null;
this._routes.forEach((route) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const pattern = new URLPattern({ pathname: route.path });
const href = `${window.location.origin}${pathname}`;
const match = pattern.test(href);
if (match) {
const result = pattern.exec(href);
location = {
pathname: result.pathname.input,
params: result.pathname.groups,
fullPath: result.pathname.input,
route,
};
}
});
return location;
}
private _setupElement(location: UmbRouteLocation) {
this._element = document.createElement<any>(location.route.elementName);
this._element.location = location;
}
private async _render() {
const childNodes = this._outlet.childNodes;
childNodes.forEach((node) => {
this._outlet.removeChild(node);
});
this._outlet.appendChild(this._element);
}
}
export * from './router';
export * from './router-before-enter.event';

View File

@@ -0,0 +1,29 @@
import { UmbRouteLocation } from './router';
export const umbRouterBeforeEnterEventType = 'umb:router-before-enter';
/**
* @export
* @interface UmbRouterBeforeEnter
*/
export interface UmbRouterBeforeEnter {
readonly to: UmbRouteLocation;
}
/**
* @export
* @class UmbRouterBeforeEnterEvent
* @extends {Event}
* @implements {UmbRouterBeforeEnter}
*/
export class UmbRouterBeforeEnterEvent extends Event implements UmbRouterBeforeEnter {
public constructor (
public readonly to: UmbRouteLocation,
) {
super(umbRouterBeforeEnterEventType, {bubbles: true, composed: true, cancelable: true });
}
}
export const isUmbRouterBeforeEnterEvent = (event: Event): event is UmbRouterBeforeEnterEvent => {
return event.type === umbRouterBeforeEnterEventType;
}

View File

@@ -0,0 +1,29 @@
import { UmbRouteLocation } from './router';
export const umbRouterBeforeLeaveEventType = 'umb:router-before-leave';
/**
* @export
* @interface UmbRouterBeforeLeave
*/
export interface UmbRouterBeforeLeave {
readonly to: UmbRouteLocation;
}
/**
* @export
* @class UmbRouterBeforeLeaveEvent
* @extends {Event}
* @implements {UmbRouterBeforeLeave}
*/
export class UmbRouterBeforeLeaveEvent extends Event implements UmbRouterBeforeLeave {
public constructor (
public readonly to: UmbRouteLocation,
) {
super(umbRouterBeforeLeaveEventType, {bubbles: true, composed: true, cancelable: true });
}
}
export const isUmbRouterBeforeLeaveEvent = (event: Event): event is UmbRouterBeforeLeaveEvent => {
return event.type === umbRouterBeforeLeaveEventType;
}

View File

@@ -0,0 +1,142 @@
import { Observable, ReplaySubject } from 'rxjs';
import { UmbRouterBeforeEnterEvent } from './router-before-enter.event';
import { UmbRouterBeforeLeaveEvent } from './router-before-leave.event';
export interface UmbRoute {
path: string;
alias: string;
meta?: any;
}
export interface UmbRouteLocation {
pathname: string;
params: object;
fullPath: string;
route: UmbRoute;
}
export interface UmbRouteElement extends HTMLElement {
location?: UmbRouteLocation;
beforeEnter?: (to: UmbRouteLocation) => Promise<boolean>;
beforeLeave?: (to: UmbRouteLocation) => Promise<boolean>;
}
export class UmbRouter {
private _routes: Array<UmbRoute> = [];
private _host: HTMLElement;
private _element?: UmbRouteElement;
private _location: ReplaySubject<UmbRouteLocation> = new ReplaySubject(1);
public readonly location: Observable<UmbRouteLocation> = this._location.asObservable();
constructor(host: HTMLElement) {
this._host = host;
// Anchor Hijacker
this._host.addEventListener('click', async (event: any) => {
const target = event.composedPath()[0];
const href = target.href;
if (!href) return;
event.preventDefault();
const url = new URL(href);
const pathname = url.pathname;
this._navigate(pathname);
});
}
public setRoutes(routes: Array<UmbRoute>) {
this._routes = routes;
const pathname = window.location.pathname;
this.push(pathname);
}
public getRoutes() {
return this._routes;
}
public go(delta: number) {
history.go(delta);
}
public back() {
history.back();
}
public forward() {
history.forward();
}
public push(pathname: string) {
history.pushState(null, '', pathname);
this._navigate(pathname);
}
private async _requestLeave(to: UmbRouteLocation) {
if (typeof this._element?.beforeLeave === 'function') {
const res = await this._element.beforeLeave(to);
if (!res) return;
}
const beforeLeaveEvent = new UmbRouterBeforeLeaveEvent(to);
this._host.dispatchEvent(beforeLeaveEvent);
if (beforeLeaveEvent.defaultPrevented) return;
return true;
}
private async _requestEnter(to: UmbRouteLocation) {
if (typeof this._element?.beforeEnter === 'function') {
const res = await this._element.beforeEnter(to);
if (!res) return;
}
const beforeEnterEvent = new UmbRouterBeforeEnterEvent(to);
this._host.dispatchEvent(beforeEnterEvent);
if (beforeEnterEvent.defaultPrevented) return;
return true;
}
private async _navigate(pathname: string) {
const location = this._resolve(pathname);
if (!location) return;
const canLeave = await this._requestLeave(location);
if (!canLeave) return;
const canEnter = await this._requestEnter(location);
if (!canEnter) return;
window.history.pushState(null, '', pathname);
this._location.next(location);
}
private _resolve(pathname: string): UmbRouteLocation | null {
let location: UmbRouteLocation | null = null;
this._routes.forEach((route) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const pattern = new URLPattern({ pathname: route.path });
const href = `${window.location.origin}${pathname}`;
const match = pattern.test(href);
if (match) {
const result = pattern.exec(href);
location = {
pathname: result.pathname.input,
params: result.pathname.groups,
fullPath: result.pathname.input,
route,
};
}
});
return location;
}
}

View File

@@ -32,28 +32,36 @@ const registerInternalManifests = async () => {
alias: 'Umb.Section.Content',
name: 'Content',
elementName: 'umb-content-section',
meta: {}
meta: {
weight: 50
}
},
{
type: 'section',
alias: 'Umb.Section.Media',
name: 'Media',
elementName: 'umb-media-section',
meta: {}
meta: {
weight: 40
}
},
{
type: 'section',
alias: 'Umb.Section.Members',
name: 'Members',
elementName: 'umb-members-section',
meta: {}
meta: {
weight: 30
}
},
{
type: 'section',
alias: 'Umb.Section.Settings',
name: 'Settings',
elementName: 'umb-settings-section',
meta: {}
meta: {
weight: 20
}
}
];
manifests.forEach((manifest: UmbExtensionManifest<unknown>) => extensionRegistry.register(manifest));

View File

@@ -14,6 +14,9 @@ export const handlers = [
alias: 'My.Section.Custom',
name: 'Custom',
elementName: 'umb-custom-section',
meta: {
weight: 30
}
},
],
})

View File

@@ -0,0 +1,33 @@
import { firstValueFrom, map, Observable, ReplaySubject } from 'rxjs';
import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from './core/extension';
export class UmbSectionContext {
private _extensionRegistry!: UmbExtensionRegistry;
private _current: ReplaySubject<UmbExtensionManifest<UmbManifestSectionMeta>> = new ReplaySubject(1);
public readonly current: Observable<UmbExtensionManifest<UmbManifestSectionMeta>> = this._current.asObservable();
constructor(_extensionRegistry: UmbExtensionRegistry) {
this._extensionRegistry = _extensionRegistry;
}
getSections () {
return this._extensionRegistry.extensions
.pipe(
map((extensions: Array<UmbExtensionManifest<unknown>>) => extensions
.filter(extension => extension.type === 'section')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight))
);
}
getCurrent () {
return this.current;
}
async setCurrent (sectionAlias: string) {
const sections = await firstValueFrom(this.getSections());
const matchedSection = sections.find(section => section.alias === sectionAlias) as UmbExtensionManifest<UmbManifestSectionMeta>;
this._current.next(matchedSection);
}
}