src/lib/fs-editing-overlay/fs-editing-overlay.component.ts
This overlay component marks empty slots in that content can be added.
OnDestroy
selector | fs-fs-editing-overlay |
styleUrls | ./fs-editing-overlay.component.css |
templateUrl | ./fs-editing-overlay.component.html |
Properties |
|
Methods |
|
constructor(componentData: CmsComponentData<FsEditingOverlay>, cmsService: CmsService, tppWrapperService: TppWrapperService, previewService: PreviewService, routingService: RoutingService, changeDetectorRef: ChangeDetectorRef)
|
|||||||||||||||||||||
Parameters :
|
Async addContent |
addContent()
|
Returns :
Promise<void>
|
Private Async createPage | |||||||||
createPage(page: Page, routerState: RouterState)
|
|||||||||
Parameters :
Returns :
Promise<CreatePageResult | void>
|
Private Async createSection | |||||||||
createSection(previewElement: string, componentData: FsEditingOverlay)
|
|||||||||
Parameters :
Returns :
Promise<void>
|
Private getComponentDataWithFlexType | ||||||
getComponentDataWithFlexType(componentUid: string)
|
||||||
Parameters :
Returns :
any
|
Private getLatestPagePreviewData |
getLatestPagePreviewData()
|
Returns :
Observable<>
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
components$ |
Type : Observable<any[]>
|
isButtonDisabled |
Default value : false
|
Private subs$ |
Default value : new Subscription()
|
Static Readonly TYPE_CODE |
Type : string
|
Default value : 'FsEditingOverlay'
|
import { PreviewTranslationKey as TranslationKey } from '../fs/cms/page/preview/preview-translation.service';
import { Component, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { CmsComponent, CmsService, RoutingService, Page, RouterState, PageType } from '@spartacus/core';
import { CmsComponentData } from '@spartacus/storefront';
import { combineLatest, Observable, from, of, Subscription } from 'rxjs';
import { map, switchMap, take, withLatestFrom } from 'rxjs/operators';
import { TppWrapperService } from '../fs/cms/page/tpp-wrapper-service';
import { nullSafe, errorToString } from 'fs-spartacus-common';
import { PreviewService } from '../fs/cms/page/preview/preview.service';
import { extractPageUniqueId } from '../fs/util/occ-cms-pages';
import { CreatePageResult } from '../fs/cms/page/fs-tpp-api.data';
/**
* This overlay component marks empty slots in that content can be added.
*/
@Component({
selector: 'fs-fs-editing-overlay',
templateUrl: './fs-editing-overlay.component.html',
styleUrls: ['./fs-editing-overlay.component.css'],
standalone: false
})
export class FsEditingOverlayComponent implements OnDestroy {
static readonly TYPE_CODE = 'FsEditingOverlay';
isButtonDisabled = false;
private subs$ = new Subscription();
components$: Observable<any[]>;
constructor(
private componentData: CmsComponentData<FsEditingOverlay>,
private cmsService: CmsService,
private tppWrapperService: TppWrapperService,
private previewService: PreviewService,
private routingService: RoutingService,
private changeDetectorRef: ChangeDetectorRef
) {
// components$ and getComponentDataWithFlexType were copied from 'tab-paragraph-container.component.ts'
// in the Spartacus storefrontlib.
this.components$ = this.componentData.data$.pipe(
switchMap((data) =>
combineLatest([
data != null
? data.components
.split(',')
.map((component) => component.trim())
.filter((component) => '' !== component)
.map((component) => this.getComponentDataWithFlexType(component))
: of([]),
])
)
);
}
ngOnDestroy(): void {
if (this.subs$) {
this.subs$.unsubscribe();
}
}
private getComponentDataWithFlexType(componentUid: string) {
return this.cmsService.getComponentData<any>(componentUid).pipe(
map((componentData) => {
if (!componentData.flexType) {
componentData = {
...componentData,
flexType: componentData.typeCode,
};
}
return {
...componentData,
};
})
);
}
private async createSection(previewElement: string, componentData: FsEditingOverlay): Promise<void> {
return this.previewService.createSection(previewElement, componentData);
}
private async createPage(page: Page, routerState: RouterState): Promise<CreatePageResult | void> {
if (page != null && routerState != null && routerState.state != null && routerState.state.context != null) {
const pageData = extractPageUniqueId(routerState.state.context);
if (pageData != null) {
return this.previewService.createPage(pageData.pageId, page.template, routerState.state.context.type).catch((createPageError) => {
this.previewService.showDetailedErrorDialog(TranslationKey.CREATE_PAGE_UNEXPECTED_ERROR, {
errorMessage: errorToString(createPageError),
});
});
} else {
this.previewService.showErrorDialog(TranslationKey.MISSING_ROUTING_DATA);
}
} else {
this.previewService.showErrorDialog(TranslationKey.MISSING_PAGE_DATA);
}
}
private getLatestPagePreviewData(): Observable<[string, [FsEditingOverlay, Page, RouterState]]> {
// In theory, this.tppWrapperService.getPreviewElement() and this.componentData.data$ could emit more than one value.
// But of course we only want to create a section once.
// Therefore we get the latest value from each Observable, but only subscribe to one combination of the results.
const previewElement$ = from(this.tppWrapperService.getPreviewElement());
const currentPage$ = this.cmsService.getCurrentPage();
return previewElement$.pipe(
withLatestFrom(combineLatest([this.componentData.data$, currentPage$, this.routingService.getRouterState()])),
take(1)
);
}
async addContent(): Promise<void> {
if (!this.isButtonDisabled) {
this.isButtonDisabled = true;
this.subs$.add(
this.getLatestPagePreviewData().subscribe(async (pagePreviewData) => {
const [previewElement, componentDataAndPage] = nullSafe(pagePreviewData, []);
const [componentData, page, routerState] = nullSafe(componentDataAndPage, []);
if (await this.previewService.isFirstSpiritManagedPage(previewElement)) {
await this.createSection(previewElement, componentData);
} else {
const createPageResult = (await this.createPage(page, routerState)) as CreatePageResult;
if (createPageResult != null && createPageResult.identifier != null) {
const { uid, identifier, displayname, name } = createPageResult;
console.log(
`Successfully created the page '${displayname || name}' (template: ${
page.template
}, uid: ${uid}, identifier: ${identifier})`
);
const hybrisPageId = `${routerState.state.context.type || PageType.CONTENT_PAGE}:${routerState.state.context.id}`;
await this.tppWrapperService.setHybrisPageId(uid, hybrisPageId);
await this.tppWrapperService.setPreviewElement(identifier);
await this.tppWrapperService.triggerRerenderView();
await this.createSection(identifier, componentData);
} else {
console.log('The creation of the page was cancelled.');
}
}
this.isButtonDisabled = false;
this.changeDetectorRef.detectChanges();
})
);
}
}
}
export interface FsEditingOverlay extends CmsComponent {
components?: string;
slotName: string;
}
<ng-container *ngIf="components$ | async">
<div
#fsContainer
class="fs-content-connect-slot
fs-content-connect-editing-container
fs-editing
{{ (components$ | async).length <= 0 ? 'fs-content-connect-highlight-content-area' : '' }}"
data-fs-content-editing
>
<div class="fs-container-overlay"></div>
<div class="fs-content-connect-button-container">
<button class="fs-content-connect-editing-button" [disabled]="isButtonDisabled" (click)="addContent()">
<span class="fs-content-connect-icon fs-content-connect-icon-add"></span>
<span class="fs-content-connect-button-label">{{ (components$ | async).length > 0 ? 'override content' : 'add content' }}</span>
</button>
</div>
<div class="fs-content-connect-content-container">
<ng-container *ngFor="let component of components$ | async">
<ng-template [cxOutlet]="component.flexType" [cxOutletContext]="{}" [cxComponentWrapper]="component"> </ng-template>
</ng-container>
</div>
</div>
</ng-container>
./fs-editing-overlay.component.css
.fs-content-connect-slot {
position: relative;
}
.fs-content-connect-editing-container {
min-height: 6rem;
}
.fs-editing {
height: 100%;
}
.fs-content-connect-editing-container .fs-content-connect-button-container {
width: 100%;
position: absolute;
top: 50%;
z-index: 1000;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.fs-content-connect-content-container {
display: flex;
flex-wrap: wrap;
height: 100%;
}
.fs-content-connect-editing-container .fs-content-connect-content-container {
filter: contrast(50%);
}
.fs-content-connect-editing-container .fs-content-connect-content-container::after {
display: block;
clear: both;
content: '';
}
.fs-content-connect-editing-container:hover .fs-content-connect-content-container {
filter: contrast(70%);
transition: all 0.2s ease-in-out;
}
@media screen and (prefers-reduced-motion: reduce) {
.fs-content-connect-editing-container:hover .fs-content-connect-content-container {
transition: none;
}
}
.fs-content-connect-editing-container.fs-content-connect-highlight-content-area {
border: 0.15rem transparent solid;
}
.fs-content-connect-editing-container.fs-content-connect-highlight-content-area:hover {
background-color: rgba(108, 117, 125, 0.3);
}
.fs-content-connect-editing-container.fs-content-connect-highlight-content-area .fs-content-connect-button-container {
position: relative;
top: 0;
transform: translateY(0%);
}
.fs-content-connect-editing-container .fs-content-connect-editing-button {
display: block;
width: auto;
min-height: 3.9rem;
min-width: 3.9rem;
margin: 1rem auto;
position: relative;
top: 50%;
color: #fff;
font-size: 2.25rem;
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-weight: 300;
background-color: #3288c3;
border-radius: 3.65rem;
border: 0.15rem #3288c3 solid;
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE10+/Edge */
user-select: none;
/* Standard */
}
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-icon {
color: #fff;
margin-top: -0.3rem;
box-sizing: border-box;
display: inline-block;
height: 1em;
width: 1em;
position: relative;
font-size: inherit;
font-style: normal;
text-indent: -9999px;
vertical-align: middle;
}
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-icon::before,
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-icon::after {
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-icon.fs-content-connect-icon-add::before {
content: '';
background: currentColor;
height: 0.2rem;
width: 100%;
transform: translate(-50%, -50%);
}
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-icon.fs-content-connect-icon-add::after {
content: '';
background: currentColor;
height: 100%;
width: 0.2rem;
transform: translate(-50%, -50%);
}
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-button-label {
display: inline-block;
max-width: 0;
opacity: 0;
white-space: nowrap;
transition: 1s, opacity 0.2s;
}
@media screen and (prefers-reduced-motion: reduce) {
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-button-label {
transition: none;
}
}
.fs-content-connect-editing-container .fs-content-connect-editing-button:hover {
background-color: #266895;
border-color: #266895;
}
.fs-content-connect-editing-container
.fs-content-connect-button-container:hover
.fs-content-connect-editing-button
.fs-content-connect-button-label {
opacity: 1;
padding: 0 0.8rem;
max-width: 50rem;
transition: max-width 2s ease-out 0.1s, opacity 1s ease-out 0.5s;
}
@media screen and (prefers-reduced-motion: reduce) {
.fs-content-connect-editing-container .fs-content-connect-editing-button .fs-content-connect-button-label:hover {
transition: none;
}
}
.fs-container-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgb(108, 117, 125);
opacity: 0.45;
}
.fs-content-connect-slot:hover .fs-container-overlay {
opacity: 0.3;
transition: opacity 0.2s ease-in-out;
}
@media screen and (prefers-reduced-motion: reduce) {
.fs-content-connect-slot:hover .fs-container-overlay {
transition: none;
}
}