From 58eb1ad2840c6f44c014738d09b649a340e97d7e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 24 May 2022 14:10:55 +0200 Subject: [PATCH 1/6] create router events --- src/Umbraco.Web.UI.Client/src/app.ts | 33 +++- .../src/core/router/index.ts | 166 +----------------- .../core/router/router-before-enter.event.ts | 29 +++ .../core/router/router-before-leave.event.ts | 29 +++ .../src/core/router/router.ts | 163 +++++++++++++++++ 5 files changed, 248 insertions(+), 172 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/core/router/router-before-enter.event.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/router/router-before-leave.event.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/router/router.ts diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 56ce7d5526..c751824654 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -9,21 +9,24 @@ import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { getInitStatus } from './api/fetcher'; -import { UmbRoute, UmbRouter } from './core/router'; +import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; import { UmbContextProvideMixin } from './core/context'; const routes: Array = [ { path: '/login', elementName: 'umb-login', + meta: { requiresAuth: false }, }, { path: '/install', elementName: 'umb-installer', + meta: { requiresAuth: false }, }, { path: '/section/:section', elementName: 'umb-backoffice', + meta: { requiresAuth: true }, }, ]; @@ -39,14 +42,11 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } `; - @state() - _authorized = false; - _router?: UmbRouter; constructor() { super(); - this._authorized = sessionStorage.getItem('is-authenticated') === 'true'; + this.addEventListener(umbRouterBeforeEnterEventType, this._onBeforeEnter); } connectedCallback(): void { @@ -54,6 +54,22 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { this.provide('umbExtensionRegistry', window.Umbraco.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 { const outlet = this.shadowRoot?.getElementById('outlet'); if (!outlet) return; @@ -71,11 +87,12 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { this._router.push('/install'); return; } - - if (!this._authorized) { + + if (!this._isAuthorized()) { this._router.push('/login'); } else { - this._router.push('/section/content'); + const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname; + this._router.push(next); } } catch (error) { diff --git a/src/Umbraco.Web.UI.Client/src/core/router/index.ts b/src/Umbraco.Web.UI.Client/src/core/router/index.ts index 47054cda18..aa4092a4c2 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/index.ts @@ -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 = []; - private _host: HTMLElement; - private _outlet: HTMLElement; - private _element: any; - - private _location: ReplaySubject = new ReplaySubject(1); - public readonly location: Observable = 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) { - 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(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'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-before-enter.event.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-before-enter.event.ts new file mode 100644 index 0000000000..abdd3ba8d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-before-enter.event.ts @@ -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; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router-before-leave.event.ts b/src/Umbraco.Web.UI.Client/src/core/router/router-before-leave.event.ts new file mode 100644 index 0000000000..33cc029f19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/router/router-before-leave.event.ts @@ -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; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router.ts b/src/Umbraco.Web.UI.Client/src/core/router/router.ts new file mode 100644 index 0000000000..dabf89de10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/router/router.ts @@ -0,0 +1,163 @@ +import { Observable, ReplaySubject } from 'rxjs'; +import { UmbRouterBeforeEnterEvent } from './router-before-enter.event'; +import { UmbRouterBeforeLeaveEvent } from './router-before-leave.event'; + +export interface UmbRoute { + path: string; + elementName: 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; + beforeLeave?: (to: UmbRouteLocation) => Promise; +} + +export class UmbRouter { + private _routes: Array = []; + private _host: HTMLElement; + private _outlet: HTMLElement; + private _element?: UmbRouteElement; + + private _location: ReplaySubject = new ReplaySubject(1); + public readonly location: Observable = 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; + + this._navigate(pathname); + }); + } + + public setRoutes(routes: Array) { + 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; + + 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(location.route.elementName); + this._element.location = location; + } + + private async _render() { + if (!this._element) return; + + const childNodes = this._outlet.childNodes; + childNodes.forEach((node) => { + this._outlet.removeChild(node); + }); + + this._outlet.appendChild(this._element); + } +} From d9ee7081c54e82fe97c20c52c21b9a4e9283041b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 24 May 2022 22:17:09 +0200 Subject: [PATCH 2/6] render section element when section changes --- src/Umbraco.Web.UI.Client/src/app.ts | 73 ++++++++++++---- .../src/auth/login/login.element.ts | 2 +- .../backoffice/backoffice-header.element.ts | 85 ++++++++++++------- .../src/backoffice/backoffice-main.element.ts | 62 +++++++++++++- .../src/core/router/router.ts | 25 +----- .../src/section.context.ts | 31 +++++++ 6 files changed, 208 insertions(+), 70 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/section.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index c751824654..d38597414c 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -1,31 +1,36 @@ 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 { UmbSectionContext } from './section.context'; import { css, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { getInitStatus } from './api/fetcher'; -import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; +import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; import { UmbContextProvideMixin } from './core/context'; +import { Subscription } from 'rxjs'; const routes: Array = [ { 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 }, }, ]; @@ -42,7 +47,11 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } `; - _router?: UmbRouter; + private _isInstalled = false; + + private _view?: HTMLElement; + private _router?: UmbRouter; + private _locationSubscription?: Subscription; constructor() { super(); @@ -51,7 +60,10 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { connectedCallback(): void { super.connectedCallback(); + const { extensionRegistry } = window.Umbraco; + this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry); + this.provide('umbSectionContext', new UmbSectionContext(extensionRegistry)); } private _onBeforeEnter = (event: Event) => { @@ -71,10 +83,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { } protected async firstUpdated(): Promise { - 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 @@ -83,27 +92,59 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { try { const { data } = await getInitStatus({}); - if (!data.installed) { + this._isInstalled = data.installed; + + if (!this._isInstalled) { this._router.push('/install'); return; } - if (!this._isAuthorized()) { + if (!this._isAuthorized() || window.location.pathname === '/install') { this._router.push('/login'); } else { - const next = window.location.pathname === '/' ? '/section/content' : window.location.pathname; + 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` -
- `; + return html`${this._view}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts index 115e9c718c..8f9a9801d8 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts @@ -71,7 +71,7 @@ export class UmbLogin extends UmbContextInjectMixin(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; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts index 43329b3cae..8ac71d3e6b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts @@ -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 { UmbContextInjectMixin } from '../core/context'; -import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from '../core/extension'; -import { UmbRouter } from '../core/router'; +import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension'; +import { UmbRouteLocation, UmbRouter } from '../core/router'; +import { UmbSectionContext } from '../section.context'; // TODO: umb or not umb in file name? @@ -81,9 +82,6 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { @state() private _open = false; - @state() - private _availableSections: Array> = []; - @state() private _allowedSection: Array = []; @@ -97,11 +95,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { private _extraSections: Array> = []; @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; private _handleMore(e: MouseEvent) { e.stopPropagation(); @@ -118,12 +119,14 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(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'); @@ -135,40 +138,64 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { super.connectedCallback(); this.requestContext('umbRouter'); - this.requestContext('umbExtensionRegistry'); + this.requestContext('umbSectionContext'); } contextInjected(contexts: Map): void { - if (contexts.has('umbExtensionRegistry')) { - this._extensionRegistry = contexts.get('umbExtensionRegistry'); - this._useSections(); - } - if (contexts.has('umbRouter')) { this._router = contexts.get('umbRouter'); + this._useLocation(); + } + + if (contexts.has('umbSectionContext')) { + this._sectionContext = contexts.get('umbSectionContext'); + this._useCurrentSection(); + this._useSections(); } } + private _useLocation () { + this._locationSubscription?.unsubscribe(); + + this._locationSubscription = this._router?.location + .subscribe((location: UmbRouteLocation) => { + this._location = location; + }); + } + + 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>) => - 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, @@ -183,7 +210,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { ${this._extraSections.map( (section) => html` ` @@ -207,7 +234,7 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { ${this._visibleSections.map( (section) => html` ` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts index af915b7996..fd9b98be03 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts @@ -1,9 +1,20 @@ 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 { UmbContextInjectMixin } 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 UmbContextInjectMixin(LitElement) { static styles = [ UUITextStyles, css` @@ -70,8 +81,56 @@ export class UmbBackofficeMain extends LitElement { `, ]; + @state() + private _sectionElement?: HTMLElement; + + private _sectionContext?: UmbSectionContext; + private _currentSectionSubscription?: Subscription; + + connectedCallback(): void { + super.connectedCallback(); + this.requestContext('umbRouter'); + this.requestContext('umbSectionContext'); + } + + contextInjected(contexts: Map): void { + if (contexts.has('umbSectionContext')) { + this._sectionContext = contexts.get('umbSectionContext'); + 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) { + 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` + ${ this._sectionElement } + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/core/router/router.ts b/src/Umbraco.Web.UI.Client/src/core/router/router.ts index dabf89de10..50d2746711 100644 --- a/src/Umbraco.Web.UI.Client/src/core/router/router.ts +++ b/src/Umbraco.Web.UI.Client/src/core/router/router.ts @@ -4,7 +4,7 @@ import { UmbRouterBeforeLeaveEvent } from './router-before-leave.event'; export interface UmbRoute { path: string; - elementName: string; + alias: string; meta?: any; } @@ -24,15 +24,13 @@ export interface UmbRouteElement extends HTMLElement { export class UmbRouter { private _routes: Array = []; private _host: HTMLElement; - private _outlet: HTMLElement; private _element?: UmbRouteElement; private _location: ReplaySubject = new ReplaySubject(1); public readonly location: Observable = this._location.asObservable(); - constructor(host: HTMLElement, outlet: HTMLElement) { + constructor(host: HTMLElement) { this._host = host; - this._outlet = outlet; // Anchor Hijacker this._host.addEventListener('click', async (event: any) => { @@ -110,15 +108,12 @@ export class UmbRouter { const canLeave = await this._requestLeave(location); if (!canLeave) 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 { @@ -144,20 +139,4 @@ export class UmbRouter { return location; } - - private _setupElement(location: UmbRouteLocation) { - this._element = document.createElement(location.route.elementName); - this._element.location = location; - } - - private async _render() { - if (!this._element) return; - - const childNodes = this._outlet.childNodes; - childNodes.forEach((node) => { - this._outlet.removeChild(node); - }); - - this._outlet.appendChild(this._element); - } } diff --git a/src/Umbraco.Web.UI.Client/src/section.context.ts b/src/Umbraco.Web.UI.Client/src/section.context.ts new file mode 100644 index 0000000000..7062fade4f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/section.context.ts @@ -0,0 +1,31 @@ +import { firstValueFrom, map, Observable, ReplaySubject } from 'rxjs'; +import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from './core/extension'; + +export class UmbSectionContext { + private _extensionRegistry!: UmbExtensionRegistry; + + private _current: ReplaySubject> = new ReplaySubject(1); + public readonly current: Observable> = this._current.asObservable(); + + constructor(_extensionRegistry: UmbExtensionRegistry) { + this._extensionRegistry = _extensionRegistry; + } + + getSections () { + return this._extensionRegistry.extensions + .pipe( + map((extensions: Array>) => extensions.filter(extension => extension.type === 'section')) + ); + } + + getCurrent () { + return this.current; + } + + async setCurrent (sectionAlias: string) { + const sections = await firstValueFrom(this.getSections()); + const matchedSection = sections.find(section => section.alias === sectionAlias) as UmbExtensionManifest; + this._current.next(matchedSection); + } + +} \ No newline at end of file From e3d4776a19c2a2cb53f1c569b33b44064974d9b7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 24 May 2022 22:36:50 +0200 Subject: [PATCH 3/6] render editor as part of content section --- src/Umbraco.Web.UI.Client/src/app.ts | 4 +- .../src/backoffice/backoffice-main.element.ts | 75 +------------ .../src/backoffice/backoffice.element.ts | 5 +- .../src/backoffice/node-editor.element.ts | 100 ++++++++++++++++++ .../src/content/content-section.element.ts | 10 +- 5 files changed, 113 insertions(+), 81 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index d38597414c..f6b68c8e97 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -6,11 +6,12 @@ import './installer/installer.element'; import './auth/login/login.element'; import './auth/auth-layout.element'; import './backoffice/backoffice.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 { getInitStatus } from './api/fetcher'; import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router'; @@ -89,6 +90,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { // TODO: find a solution for magic strings this.provide('umbRouter', this._router); + // TODO: this is a temporary routing solution for shell elements try { const { data } = await getInitStatus({}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts index fd9b98be03..f5d2d48995 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts @@ -19,64 +19,9 @@ export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) { 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; } `, ]; @@ -130,24 +75,6 @@ export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) { render() { return html` ${ this._sectionElement } - `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 7cbf619ba2..3e0c6b7890 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -35,10 +35,7 @@ export class UmbBackoffice extends LitElement { render() { return html` -
- - -
+ `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts new file mode 100644 index 0000000000..b787cc0c6c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts @@ -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` +
+
+ + + Content + Info + Actions + +
+ +
+ Save and preview + Save + Save and publish +
+
+ ` + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-node-editor': UmbNodeEditor; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts b/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts index b0ba6a734e..195756b124 100644 --- a/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/content/content-section.element.ts @@ -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`
Content Section
`; + return html``; } } From 01e22d1bcfc8507836cb8d382a870df772c20ffe Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 25 May 2022 08:52:33 +0200 Subject: [PATCH 4/6] order sections by weight --- src/Umbraco.Web.UI.Client/src/index.ts | 16 ++++++++++++---- src/Umbraco.Web.UI.Client/src/mocks/handlers.ts | 3 +++ src/Umbraco.Web.UI.Client/src/section.context.ts | 4 +++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/index.ts b/src/Umbraco.Web.UI.Client/src/index.ts index b0321b2c59..4d1399091f 100644 --- a/src/Umbraco.Web.UI.Client/src/index.ts +++ b/src/Umbraco.Web.UI.Client/src/index.ts @@ -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) => extensionRegistry.register(manifest)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts index 46a53d6680..5a74754723 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers.ts @@ -14,6 +14,9 @@ export const handlers = [ alias: 'My.Section.Custom', name: 'Custom', elementName: 'umb-custom-section', + meta: { + weight: 30 + } }, ], }) diff --git a/src/Umbraco.Web.UI.Client/src/section.context.ts b/src/Umbraco.Web.UI.Client/src/section.context.ts index 7062fade4f..4db9d6ecdc 100644 --- a/src/Umbraco.Web.UI.Client/src/section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/section.context.ts @@ -14,7 +14,9 @@ export class UmbSectionContext { getSections () { return this._extensionRegistry.extensions .pipe( - map((extensions: Array>) => extensions.filter(extension => extension.type === 'section')) + map((extensions: Array>) => extensions + .filter(extension => extension.type === 'section') + .sort((a: any, b: any) => b.meta.weight - a.meta.weight)) ); } From e1a73d62be656605261f6cf23df9960d04cbcba5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 25 May 2022 13:16:17 +0200 Subject: [PATCH 5/6] prefix unused vars --- .../src/core/context/context-consumer.mixin.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts index 311f0be711..1a7e2f6598 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts @@ -68,13 +68,12 @@ export const UmbContextConsumerMixin = >(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. } }; From fbdd3829796527c9328c9ad55e6db89b882f4410 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 25 May 2022 13:37:39 +0200 Subject: [PATCH 6/6] update to new context api names --- .../src/backoffice/backoffice-main.element.ts | 18 +++++++----------- .../src/core/context/context-consumer.mixin.ts | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts index f5d2d48995..8dcbc9c0b6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-main.element.ts @@ -4,7 +4,7 @@ import { css, html, LitElement } from 'lit'; import { state } from 'lit/decorators.js'; import { Subscription } from 'rxjs'; -import { UmbContextInjectMixin } from '../core/context'; +import { UmbContextConsumerMixin } from '../core/context'; import { UmbSectionContext } from '../section.context'; import { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension'; @@ -14,7 +14,7 @@ import '../content/content-section.element'; import '../media/media-section.element'; @defineElement('umb-backoffice-main') -export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) { +export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { static styles = [ UUITextStyles, css` @@ -32,17 +32,13 @@ export class UmbBackofficeMain extends UmbContextInjectMixin(LitElement) { private _sectionContext?: UmbSectionContext; private _currentSectionSubscription?: Subscription; - connectedCallback(): void { - super.connectedCallback(); - this.requestContext('umbRouter'); - this.requestContext('umbSectionContext'); - } + constructor () { + super(); - contextInjected(contexts: Map): void { - if (contexts.has('umbSectionContext')) { - this._sectionContext = contexts.get('umbSectionContext'); + this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => { + this._sectionContext = _instance; this._useCurrentSection(); - } + }); } private _useCurrentSection () { diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts index 1a7e2f6598..3a6687d205 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts @@ -3,7 +3,7 @@ import { UmbContextConsumer } from './context-consumer'; type Constructor = 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; }