-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ICM 11): experimental Design View support for the PWA (within th…
…e Intershop Administration Portal) (#1462) * required functionality for the Intershop PWA to interact with the new IAP Design View * wrapper for highlighting Pagelets, Slots and Includes * enable Design View highlighting with 'DESIGNVIEW' PreviewContextId * highlight only last element in hierarchy * send post message event to Design View whenever the language has changed * some basic documentation Co-authored-by: Andreas Steinmann <asteinmann@intershop.com> Co-authored-by: Silke <s.grueber@intershop.de> Co-authored-by: Marcel Eisentraut <meisentraut@intershop.de> Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
- Loading branch information
1 parent
e7346b5
commit 8452343
Showing
20 changed files
with
566 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
src/app/core/utils/design-view/design-view.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { provideMockStore } from '@ngrx/store/testing'; | ||
|
||
import { getCurrentLocale } from 'ish-core/store/core/configuration'; | ||
|
||
import { DesignViewService } from './design-view.service'; | ||
|
||
describe('Design View Service', () => { | ||
let designViewService: DesignViewService; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
provideMockStore({ | ||
selectors: [{ selector: getCurrentLocale, value: 'en_US' }], | ||
}), | ||
], | ||
}); | ||
designViewService = TestBed.inject(DesignViewService); | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(designViewService).toBeTruthy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import { ApplicationRef, Injectable } from '@angular/core'; | ||
import { NavigationEnd, Router } from '@angular/router'; | ||
import { Store, select } from '@ngrx/store'; | ||
import { filter, first, fromEvent, map, switchMap } from 'rxjs'; | ||
|
||
import { getCurrentLocale } from 'ish-core/store/core/configuration'; | ||
import { DomService } from 'ish-core/utils/dom/dom.service'; | ||
import { whenTruthy } from 'ish-core/utils/operators'; | ||
|
||
interface DesignViewMessage { | ||
type: | ||
| 'dv-clientAction' | ||
| 'dv-clientNavigation' | ||
| 'dv-clientReady' | ||
| 'dv-clientRefresh' | ||
| 'dv-clientLocale' | ||
| 'dv-clientStable'; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
payload?: any; | ||
} | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class DesignViewService { | ||
private allowedHostMessageTypes = ['dv-clientRefresh']; | ||
|
||
constructor( | ||
private router: Router, | ||
private appRef: ApplicationRef, | ||
private domService: DomService, | ||
private store: Store | ||
) { | ||
this.init(); | ||
} | ||
|
||
/** | ||
* Send a message to the host window. | ||
* Send the message to any host since the PWA is not supposed to know a fixed IAP URL (we are not sending secrets). | ||
* | ||
* @param message The message to send to the host (including type and payload) | ||
*/ | ||
messageToHost(message: DesignViewMessage) { | ||
window.parent.postMessage(message, '*'); | ||
} | ||
|
||
/** | ||
* Start method that sets up Design View communication. | ||
* Needs to be called *once* for the whole application. | ||
*/ | ||
private init() { | ||
if (!this.shouldInit()) { | ||
return; | ||
} | ||
|
||
this.listenToHostMessages(); | ||
this.listenToApplication(); | ||
|
||
// tell the host client is ready | ||
this.messageToHost({ type: 'dv-clientReady' }); | ||
} | ||
|
||
/** | ||
* Decides whether to init the Design View capabilities or not. | ||
* Is used by the init method, so it will only initialize when | ||
* (1) there is a window (i.e. the application does not run in SSR/Universal) | ||
* (2) application does not run on top level window (i.e. it runs in the Design View iframe) | ||
*/ | ||
private shouldInit() { | ||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain | ||
return typeof window !== 'undefined' && window.parent && window.parent !== window; | ||
} | ||
|
||
/** | ||
* Subscribe to messages from the host window. | ||
* Incoming messages are filtered using `allowedHostMessageTypes` | ||
* Should only be called *once* during initialization. | ||
*/ | ||
private listenToHostMessages() { | ||
fromEvent<MessageEvent>(window, 'message') | ||
.pipe( | ||
filter(e => e.data.hasOwnProperty('type') && this.allowedHostMessageTypes.includes(e.data.type)), | ||
map(message => message.data) | ||
) | ||
.subscribe(message => this.handleHostMessage(message)); | ||
} | ||
|
||
/** | ||
* Listen to events throughout the application and send message to host when | ||
* (1) route has changed (`dv-clientNavigation`), | ||
* (2) application is stable, i.e. all async tasks have been completed (`dv-clientStable`) or | ||
* (3) content include has been reloaded (`dv-clientStable`). | ||
* | ||
* Should only be called *once* during initialization. | ||
*/ | ||
private listenToApplication() { | ||
const navigation$ = this.router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd)); | ||
const stable$ = this.appRef.isStable.pipe(whenTruthy(), first()); | ||
const navigationStable$ = navigation$.pipe(switchMap(() => stable$)); | ||
|
||
// send `dv-clientNavigation` event for each route change | ||
navigation$.subscribe(e => this.messageToHost({ type: 'dv-clientNavigation', payload: { url: e.url } })); | ||
|
||
stable$.subscribe(() => { | ||
this.applyHierarchyHighlighting(); | ||
}); | ||
|
||
// send `dv-clientStable` event when application is stable or loading of the content included finished | ||
navigationStable$.subscribe(() => { | ||
this.messageToHost({ type: 'dv-clientStable' }); | ||
this.applyHierarchyHighlighting(); | ||
}); | ||
|
||
// send `dv-clientLocale` event when application is stable and the current application locale was determined | ||
stable$ | ||
.pipe( | ||
switchMap(() => | ||
this.store.pipe(select(getCurrentLocale), whenTruthy()).pipe( | ||
first() // PWA reloads after each locale change, only one locale is active during runtime | ||
) | ||
) | ||
) | ||
.subscribe(locale => this.messageToHost({ type: 'dv-clientLocale', payload: { locale } })); | ||
} | ||
|
||
/** | ||
* Handle incoming message from the host window. | ||
* Invoked by the event listener in `listenToHostMessages()` when a new message arrives. | ||
*/ | ||
private handleHostMessage(message: DesignViewMessage) { | ||
switch (message.type) { | ||
case 'dv-clientRefresh': { | ||
location.reload(); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Workaround for the missing Firefox CSS support for :has to highlight | ||
* only the last .design-view-wrapper in the .design-view-wrapper hierarchy. | ||
* | ||
*/ | ||
private applyHierarchyHighlighting() { | ||
const designViewWrapper: NodeListOf<HTMLElement> = document.querySelectorAll('.design-view-wrapper'); | ||
|
||
designViewWrapper.forEach(element => { | ||
if (!element.querySelector('.design-view-wrapper')) { | ||
this.domService.addClass(element, 'last-design-view-wrapper'); | ||
} | ||
}); | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
...red/cms/components/content-design-view-wrapper/content-design-view-wrapper.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<ng-container *ngIf="isDesignViewMode && type; else cmsOutlet"> | ||
<div class="design-view-wrapper" [ngClass]="type"> | ||
<div class="design-view-wrapper-actions"> | ||
<ng-container [ngSwitch]="type"> | ||
<!-- pagelet --> | ||
<ng-template [ngSwitchCase]="'pagelet'"> | ||
<ng-container *ngIf="pagelet$ | async as pagelet"> | ||
<div class="name">{{ pagelet.displayName ? pagelet.displayName : '(Language missing)' }}</div> | ||
<button | ||
(click)="triggerAction(pagelet.id, 'pageletEdit')" | ||
class="btn" | ||
title="{{ 'designview.edit.link.title' | translate }}" | ||
> | ||
<fa-icon [icon]="['fas', 'pencil-alt']" /> | ||
</button> | ||
<!-- | ||
<button | ||
(click)="triggerAction(pagelet.id, 'pageletDelete')" | ||
class="btn" | ||
title="{{ 'designview.delete.link.title' | translate }}" | ||
> | ||
<fa-icon [icon]="['fas', 'trash-alt']"></fa-icon> | ||
</button> | ||
--> | ||
</ng-container> | ||
</ng-template> | ||
|
||
<!-- slot --> | ||
<ng-template [ngSwitchCase]="'slot'"> | ||
<div class="name">{{ pagelet.slot(this.slotId).displayName }}</div> | ||
<button | ||
(click)="triggerAction(this.slotId, 'slotAdd')" | ||
class="btn" | ||
title="{{ 'designview.add.link.title' | translate }}" | ||
> | ||
<fa-icon [icon]="['fas', 'plus']" /> | ||
</button> | ||
</ng-template> | ||
<!-- include --> | ||
<ng-template [ngSwitchCase]="'include'"> | ||
<div class="name">{{ include.displayName }}</div> | ||
<button | ||
(click)="triggerAction(include.id, 'includeAdd')" | ||
class="btn" | ||
title="{{ 'designview.add.link.title' | translate }}" | ||
> | ||
<fa-icon [icon]="['fas', 'plus']" /> | ||
</button> | ||
</ng-template> | ||
</ng-container> | ||
</div> | ||
<ng-container *ngTemplateOutlet="cmsOutlet" /> | ||
</div> | ||
</ng-container> | ||
|
||
<ng-template #cmsOutlet> | ||
<ng-content></ng-content> | ||
</ng-template> |
Oops, something went wrong.