Merge branch 'main' of https://github.com/umbraco/Umbraco.CMS.Backoffice
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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 }
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
100
src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts
Normal file
100
src/Umbraco.Web.UI.Client/src/backoffice/node-editor.element.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
142
src/Umbraco.Web.UI.Client/src/core/router/router.ts
Normal file
142
src/Umbraco.Web.UI.Client/src/core/router/router.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -14,6 +14,9 @@ export const handlers = [
|
||||
alias: 'My.Section.Custom',
|
||||
name: 'Custom',
|
||||
elementName: 'umb-custom-section',
|
||||
meta: {
|
||||
weight: 30
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
33
src/Umbraco.Web.UI.Client/src/section.context.ts
Normal file
33
src/Umbraco.Web.UI.Client/src/section.context.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user