ng-enjoyhint
Version:
<!-- This file is generated by 'pnpm run build', so it can include the API docs for the library. Do not edit this manually.
789 lines (775 loc) • 40.4 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, computed, Component, Input, Pipe, TemplateRef, ContentChild, EventEmitter, effect, ViewChild, HostBinding, Injector, Injectable } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { combineLatest, startWith, pairwise, map, fromEvent, from, filter, switchMap, firstValueFrom } from 'rxjs';
import { trigger, transition, style, animate } from '@angular/animations';
import * as i2 from '@angular/cdk/overlay';
import { FlexibleConnectedPositionStrategy, Overlay } from '@angular/cdk/overlay';
import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
import { NgTemplateOutlet, NgClass } from '@angular/common';
import { debounceTime, takeUntil, first } from 'rxjs/operators';
class ArrowComponent {
host;
_direction = signal(undefined);
get direction() {
return this._direction();
}
set direction(value) {
this._direction.set(value);
}
_pointToElement = signal(null);
get pointToElement() {
return this._pointToElement();
}
set pointToElement(value) {
this._pointToElement.set(value);
}
viewBox = computed(() => {
const bounds = this.bounds();
if (!bounds) {
return `0 0 0 0`;
}
return `0 0 ${bounds.width} ${bounds.height}`;
});
pathData = computed(() => {
const bounds = this.bounds() ?? { x: 0, y: 0, width: 0, height: 0 };
const absCenterX = bounds.x + bounds.width / 2;
const absCenterY = bounds.y + bounds.height / 2;
const padding = 5;
const minX = padding;
const minY = padding;
const maxX = bounds.width - padding;
const maxY = bounds.height - padding;
const width = maxX - minX;
const height = maxY - minY;
const halfWidth = width / 2;
const halfHeight = height / 2;
const direction = this._direction();
const pointToElement = this._pointToElement();
const elementCenterPoint = pointToElement?.getBoundingClientRect() ?? {
x: 0,
y: 0,
width: 0,
height: 0,
};
const elementCenterX = elementCenterPoint.x + elementCenterPoint.width / 2;
const elementCenterY = elementCenterPoint.y + elementCenterPoint.height / 2;
const dx = elementCenterX - absCenterX;
const dy = elementCenterY - absCenterY;
let fromX = 0, fromY = 0, toX = 0, toY = 0, controlPointX = 0, controlPointY = 0;
switch (direction) {
case 'top':
fromX = halfWidth;
fromY = minY;
toX = halfWidth + dx;
toY = maxY;
controlPointX = halfWidth;
controlPointY = halfHeight;
break;
case 'bottom':
fromX = halfWidth;
fromY = maxY;
toX = halfWidth + dx;
toY = minY;
controlPointX = halfWidth;
controlPointY = halfHeight;
break;
case 'left':
fromX = halfWidth;
fromY = minY;
toX = maxX;
toY = halfHeight + dy;
controlPointX = halfWidth;
controlPointY = halfHeight;
break;
case 'right':
fromX = halfWidth;
fromY = minY;
toX = minX;
toY = halfHeight + dy;
controlPointX = halfWidth;
controlPointY = halfHeight;
break;
}
toX = Math.max(minX, Math.min(maxX, toX));
toY = Math.max(minY, Math.min(maxY, toY));
return `M${fromX},${fromY} Q${controlPointX},${controlPointY} ${toX},${toY}`;
});
bounds = signal(undefined);
resizeObserver;
constructor(host, zone) {
this.host = host;
const observer = new ResizeObserver(() => {
zone.run(() => {
this.bounds.set(host.nativeElement.getBoundingClientRect());
});
});
observer.observe(host.nativeElement);
this.resizeObserver = observer;
}
ngOnDestroy() {
this.resizeObserver.unobserve(this.host.nativeElement);
this.resizeObserver.disconnect();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: ArrowComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.2", type: ArrowComponent, isStandalone: true, selector: "lib-arrow", inputs: { direction: "direction", pointToElement: "pointToElement" }, ngImport: i0, template: `
<svg [attr.viewBox]="viewBox()">
<defs>
<marker
id="arrowMarker"
viewBox="0 0 36 21"
refX="21"
refY="10"
markerUnits="strokeWidth"
orient="auto"
markerWidth="16"
markerHeight="12"
>
<path id="polyline" d="M0,0 c30,11 30,9 0,20" />
</marker>
</defs>
<path [attr.d]="pathData()" marker-end="url(#arrowMarker)" />
</svg>
`, isInline: true, styles: [":host{display:block;width:100%;height:100px}path{fill:none;stroke:#fff;stroke-width:2}\n"] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: ArrowComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-arrow', standalone: true, template: `
<svg [attr.viewBox]="viewBox()">
<defs>
<marker
id="arrowMarker"
viewBox="0 0 36 21"
refX="21"
refY="10"
markerUnits="strokeWidth"
orient="auto"
markerWidth="16"
markerHeight="12"
>
<path id="polyline" d="M0,0 c30,11 30,9 0,20" />
</marker>
</defs>
<path [attr.d]="pathData()" marker-end="url(#arrowMarker)" />
</svg>
`, styles: [":host{display:block;width:100%;height:100px}path{fill:none;stroke:#fff;stroke-width:2}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }], propDecorators: { direction: [{
type: Input
}], pointToElement: [{
type: Input
}] } });
class ForceFieldComponent {
size;
position;
eventBlackHole(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: ForceFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.2", type: ForceFieldComponent, isStandalone: true, selector: "lib-force-field", inputs: { size: "size", position: "position" }, host: { listeners: { "click": "eventBlackHole($event)", "mousedown": "eventBlackHole($event)", "touchstart": "eventBlackHole($event)" }, properties: { "style.width": "size.width + \"px\"", "style.height": "size.height + \"px\"", "style.minWidth": "size.width + \"px\"", "style.minHeight": "size.height + \"px\"", "style.maxWidth": "size.width + \"px\"", "style.maxHeight": "size.height + \"px\"", "style.top": "position.y + \"px\"", "style.left": "position.x + \"px\"" } }, ngImport: i0, template: ``, isInline: true, styles: [":host{display:block;position:fixed;transition:top .5s ease-in-out,left .5s ease-in-out}\n"] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: ForceFieldComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-force-field', standalone: true, template: ``, host: {
'[style.width]': 'size.width + "px"',
'[style.height]': 'size.height + "px"',
'[style.minWidth]': 'size.width + "px"',
'[style.minHeight]': 'size.height + "px"',
'[style.maxWidth]': 'size.width + "px"',
'[style.maxHeight]': 'size.height + "px"',
'[style.top]': 'position.y + "px"',
'[style.left]': 'position.x + "px"',
'(click)': 'eventBlackHole($event)',
'(mousedown)': 'eventBlackHole($event)',
'(touchstart)': 'eventBlackHole($event)'
}, styles: [":host{display:block;position:fixed;transition:top .5s ease-in-out,left .5s ease-in-out}\n"] }]
}], propDecorators: { size: [{
type: Input
}], position: [{
type: Input
}] } });
class Elements {
get appRoot() {
return document.querySelector('[ng-version]');
}
get body() {
return document.body;
}
}
function provideWindow() {
return { provide: Window, useValue: window };
}
function getDirection(position) {
if (!position) {
return undefined;
}
if (position.originX === 'start' && position.originY === 'center') {
return 'left';
}
if (position.originX === 'end' && position.originY === 'center') {
return 'right';
}
if (position.originX === 'center' && position.originY === 'top') {
return 'top';
}
if (position.originX === 'center' && position.originY === 'bottom') {
return 'bottom';
}
console.warn('Unknown position', position);
return undefined;
}
class DirectionPipe {
transform(position) {
return getDirection(position);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: DirectionPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.0.2", ngImport: i0, type: DirectionPipe, isStandalone: true, name: "direction" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: DirectionPipe, decorators: [{
type: Pipe,
args: [{
name: 'direction',
standalone: true,
pure: true,
}]
}] });
const rightCenter = {
// right center
originX: 'end',
overlayX: 'start',
originY: 'center',
overlayY: 'center',
};
const rightTop = {
// right top
originX: 'end',
overlayX: 'start',
originY: 'top',
overlayY: 'bottom',
};
const rightBottom = {
// right bottom
originX: 'end',
overlayX: 'start',
originY: 'bottom',
overlayY: 'top',
};
const leftCenter = {
// left center
originX: 'start',
overlayX: 'end',
originY: 'center',
overlayY: 'center',
};
const leftTop = {
// left top
originX: 'start',
overlayX: 'end',
originY: 'top',
overlayY: 'bottom',
};
const leftBottom = {
// left bottom
originX: 'start',
overlayX: 'end',
originY: 'bottom',
overlayY: 'top',
};
const topCenter = {
// top center
originX: 'center',
overlayX: 'center',
originY: 'top',
overlayY: 'bottom',
};
const bottomCenter = {
// bottom center
originX: 'center',
overlayX: 'center',
originY: 'bottom',
overlayY: 'top',
};
const ALL_POSITIONS = [
rightCenter,
rightTop,
rightBottom,
leftCenter,
leftTop,
leftBottom,
topCenter,
bottomCenter,
];
const ALL_SIDES = [rightCenter, leftCenter, topCenter, bottomCenter];
class TextOrTemplateComponent {
_value = signal(undefined);
get value() {
return this._value();
}
set value(value) {
this._value.set(value);
}
defaultTextTemplate;
stringValue = computed(() => {
const value = this._value();
return typeof value === 'string' ? value : undefined;
});
templateValue = computed(() => {
const value = this._value();
if (value instanceof TemplateRef) {
return { template: value, context: { $implicit: undefined } };
}
if (value instanceof Object && value.template instanceof TemplateRef) {
return { template: value.template, context: value.context };
}
return undefined;
});
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: TextOrTemplateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.2", type: TextOrTemplateComponent, isStandalone: true, selector: "lib-text-or-template", inputs: { value: "value" }, queries: [{ propertyName: "defaultTextTemplate", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0, template: `
@if (templateValue(); as template) {
<ng-container
*ngTemplateOutlet="template.template; context: template.context"
></ng-container>
} @else if (stringValue() && defaultTextTemplate) {
<ng-container
*ngTemplateOutlet="
defaultTextTemplate;
context: { $implicit: stringValue() }
"
></ng-container>
} @else if (stringValue()) {
<span>{{ stringValue() }}</span>
}
`, isInline: true, styles: [""], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: TextOrTemplateComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-text-or-template', standalone: true, imports: [NgTemplateOutlet], template: `
@if (templateValue(); as template) {
<ng-container
*ngTemplateOutlet="template.template; context: template.context"
></ng-container>
} @else if (stringValue() && defaultTextTemplate) {
<ng-container
*ngTemplateOutlet="
defaultTextTemplate;
context: { $implicit: stringValue() }
"
></ng-container>
} @else if (stringValue()) {
<span>{{ stringValue() }}</span>
}
` }]
}], propDecorators: { value: [{
type: Input
}], defaultTextTemplate: [{
type: ContentChild,
args: [TemplateRef]
}] } });
class EnjoyHintRef {
tutorial;
options;
constructor(tutorial, options = {}) {
this.tutorial = tutorial;
this.options = options;
}
onClose = new EventEmitter();
close(completed = false) {
this.onClose.emit(completed);
}
}
class EnjoyHintComponent {
ref;
overlayPositionBuilder;
overlay;
viewContainerRef;
destroyedRef;
zone;
instructions;
opacity;
static defaultOptions = {
padding: 5,
fontFamily: 'sans-serif',
backdropColor: 'black',
overlayZIndex: undefined,
backdropOpacity: 0.75,
foregroundColor: 'white',
nextButton: { text: 'Next' },
skipButton: { text: 'Skip' },
previousButton: { text: 'Previous' },
};
viewSize;
ffSize = computed(() => ({
width: this.viewSize().width + this.options.padding,
height: this.viewSize().height + this.options.padding,
}));
instructionsWidth = computed(() => this.viewSize().width / 4);
animating = signal(false);
focusElement = computed(() => {
const step = this.step();
if (!step?.selector) {
return null;
}
return document.querySelector(step.selector);
});
elementBounds = computed(() => {
const element = this.focusElement();
if (!element) {
return {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
return element.getBoundingClientRect();
});
ffPositions = computed(() => {
const bounds = this.elementBounds();
const size = this.viewSize();
return {
left: {
x: -1 * size.width + bounds.x - this.options.padding,
y: -1 * this.options.padding,
},
right: {
x: bounds.x + bounds.width,
y: -1 * this.options.padding,
},
top: {
x: -1 * this.options.padding,
y: -1 * size.height + bounds.y - this.options.padding,
},
bottom: {
x: -1 * this.options.padding,
y: bounds.y + bounds.height,
},
};
});
step;
options;
overlayRef = null;
positionStrategy = computed(() => {
const element = this.focusElement();
if (!element) {
return this.overlayPositionBuilder
.global()
.centerHorizontally()
.centerVertically();
}
return this.overlayPositionBuilder
.flexibleConnectedTo(element)
.withFlexibleDimensions(false)
.withPositions(ALL_SIDES);
});
position = signal(undefined);
thisIsDestroyed;
/**
* instead of doing actual padding around the focus element (which would allow clicks)
* outside of the focus element), we'll add some glow around wht focus element but still
* on the force field
*
* white must be used as anything semi-transparent or transparent will not show up
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#length
*/
focusElementHighlight = computed(() => {
const padding = this.options.padding;
const boxShadow = `0 0 ${padding / 2}px ${padding}px white`;
return boxShadow;
});
constructor(ref, window, overlayPositionBuilder, overlay, viewContainerRef, destroyedRef, zone) {
this.ref = ref;
this.overlayPositionBuilder = overlayPositionBuilder;
this.overlay = overlay;
this.viewContainerRef = viewContainerRef;
this.destroyedRef = destroyedRef;
this.zone = zone;
// Consider both the current and previous steps with transitioning state
// The new step is only effective once it is no longer transitioning
const step$ = combineLatest([
toObservable(this.ref.tutorial.step).pipe(startWith(undefined), pairwise()),
toObservable(this.ref.tutorial.transitioning)
]).pipe(debounceTime(50), // since both start and end hooks set this, make sure it's done with all transitioning
map(([[prev, curr], transition]) => {
if (transition === 'start') {
return prev;
}
return curr;
}));
// Convert back to a signal
this.step = toSignal(step$, { initialValue: undefined });
this.options = {
...EnjoyHintComponent.defaultOptions,
...ref.options,
};
this.opacity = this.options.backdropOpacity.toString();
const getSize = () => ({
width: window.innerWidth,
height: window.innerHeight,
});
this.viewSize = toSignal(fromEvent(window, 'resize').pipe(map(getSize)), {
initialValue: getSize(),
});
this.thisIsDestroyed = from(new Promise((resolve) => this.destroyedRef.onDestroy(() => resolve())));
combineLatest([toObservable(this.focusElement), toObservable(this.step)])
.pipe(takeUntil(this.thisIsDestroyed), filter(([e, s]) => !!e && !!s), switchMap(([element, step]) => fromEvent(element, step.event).pipe(filter(() => !this.animating()), first())))
.subscribe(async () => {
await this.ref.tutorial.nextStep();
if (!this.step()) {
this.close(true);
}
});
effect(() => {
const currentStep = this.step(); // even though we don't use it, this is how we make the effect dependent on the step
if (!currentStep) {
return;
}
console.log({ currentStep });
const overlayRef = this.overlayRef;
overlayRef?.detach();
overlayRef?.dispose();
this.animating.set(true);
setTimeout(() => this.createOverlay(), 750);
}, { allowSignalWrites: true });
}
createOverlay() {
console.log('creating overlay...');
const step = this.step();
const positionStrategy = this.positionStrategy();
this.focusElement();
if (!step || !positionStrategy) {
return;
}
if (positionStrategy instanceof FlexibleConnectedPositionStrategy) {
positionStrategy.positionChanges
.pipe(takeUntil(this.thisIsDestroyed))
.subscribe((x) => {
this.zone.run(() => this.position.set(x.connectionPair));
});
}
this.overlayRef?.detach();
this.overlayRef?.dispose();
this.overlayRef = null;
const overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: false,
});
const zIndex = this.options.overlayZIndex;
if (zIndex !== undefined) {
const globalOverlayWrapper = overlayRef.hostElement;
globalOverlayWrapper.style.setProperty('z-index', zIndex.toString());
}
const overlayComponent = new TemplatePortal(this.instructions, this.viewContainerRef);
overlayRef.attach(overlayComponent);
this.overlayRef = overlayRef;
this.animating.set(false);
}
previous(event) {
this.eventBlackHole(event);
this.ref.tutorial.previousStep();
}
async next(event) {
this.eventBlackHole(event);
await this.ref.tutorial.nextStep();
if (!this.step()) {
this.close(true);
}
}
skip(event) {
this.eventBlackHole(event);
this.close(false);
}
close(result = false) {
this.ref.close(result);
}
eventBlackHole(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: EnjoyHintComponent, deps: [{ token: EnjoyHintRef }, { token: Window }, { token: i2.OverlayPositionBuilder }, { token: i2.Overlay }, { token: i0.ViewContainerRef }, { token: i0.DestroyRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.2", type: EnjoyHintComponent, isStandalone: true, selector: "lib-enjoyhint", host: { properties: { "style.opacity": "this.opacity" } }, providers: [provideWindow()], viewQueries: [{ propertyName: "instructions", first: true, predicate: ["instructions"], descendants: true }], ngImport: i0, template: "@if (ffPositions(); as positions) {\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.top\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.left\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.right\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.bottom\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n}\n<!-- If we just want them to click on the 'Next' button we want to disable events over the focused element -->\n<lib-force-field\n [size]=\"elementBounds()\"\n [position]=\"elementBounds()\"\n [style.background-color]=\"'transparent'\"\n [style.pointer-events]=\"step()?.event === 'next' ? 'auto' : 'none'\"\n [style.box-shadow]=\"focusElementHighlight()\"\n>\n</lib-force-field>\n\n\n<ng-template #instructions>\n <div\n class=\"instructions\"\n @fadeIn\n [ngClass]=\"{\n top: (position() | direction) === 'top',\n left: (position() | direction) === 'left',\n right: (position() | direction) === 'right',\n bottom: (position() | direction) === 'bottom',\n }\"\n [style.maxWidth]=\"instructionsWidth()\"\n >\n <lib-text-or-template [value]=\"step()?.description\">\n <ng-template let-description>\n <label [style.fontFamily]=\"options.fontFamily\">\n {{ description }}\n </label>\n </ng-template>\n </lib-text-or-template>\n @if (step()?.details) {\n <lib-text-or-template [value]=\"step()?.details\">\n <ng-template let-details>\n <p [style.fontFamily]=\"options.fontFamily\">\n {{ details }}\n </p>\n </ng-template>\n </lib-text-or-template>\n }\n @if (focusElement(); as element) {\n <lib-arrow [direction]=\"position() | direction\" [pointToElement]=\"focusElement()\"></lib-arrow>\n }\n <div class=\"buttons\">\n @if (ref.tutorial.hasPrevious() && !step()?.hidePrevious) {\n <button\n class=\"previous\"\n (click)=\"previous($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.previousButton?.className\"\n >\n {{ step()?.previousButton?.text ?? options.previousButton.text }}\n </button>\n } @if (step()?.event === 'next') {\n <button\n class=\"next\"\n (click)=\"next($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.nextButton?.className\"\n >\n {{ step()?.nextButton?.text ?? options.nextButton.text }}\n </button>\n } @if (!step()?.hideSkip) {\n <button\n class=\"skip\"\n (click)=\"skip($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.skipButton?.className\"\n >\n {{ step()?.skipButton?.text ?? options.skipButton.text }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["label{color:#fff;font-size:2rem}p{color:#fff;font-size:1.5rem}.instructions{display:grid;grid-template-rows:repeat(4,auto);grid-template-columns:auto;gap:.5rem;justify-content:start;justify-items:start;padding:.75rem;max-width:25vw}.instructions label{grid-column:1;grid-row:1}.instructions p{grid-column:1;grid-row:2}.instructions lib-arrow{grid-column:1;grid-row:3}.instructions .buttons{grid-column:1;grid-row:4}.instructions.left{justify-content:end;justify-items:end}.instructions.right{justify-content:start;justify-items:start}.instructions.top{justify-content:center;justify-items:center}.instructions.top .buttons{grid-row:1}.instructions.top label{grid-row:2}.instructions.top p{grid-row:3}.instructions.top lib-arrow{grid-row:4}.instructions.bottom{justify-content:center;justify-items:center}.instructions.bottom lib-arrow{grid-row:1}.instructions.bottom label{grid-row:2}.instructions.bottom p{grid-row:3}.instructions.bottom .buttons{grid-row:4}.buttons{display:flex;flex-direction:row;gap:.5rem}.buttons button{background-color:transparent;border:1px solid white;border-radius:5px;color:#fff;font-size:.75rem;min-width:5rem;padding:.5rem;transition:all .25s ease-in-out;cursor:pointer}.buttons button.next{background-color:green;color:#fff}.buttons button:hover{background-color:#fff;color:#000}.peep-hole{box-shadow:0 0 5px 10px #fff}\n"], dependencies: [{ kind: "component", type: ForceFieldComponent, selector: "lib-force-field", inputs: ["size", "position"] }, { kind: "component", type: ArrowComponent, selector: "lib-arrow", inputs: ["direction", "pointToElement"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: DirectionPipe, name: "direction" }, { kind: "component", type: TextOrTemplateComponent, selector: "lib-text-or-template", inputs: ["value"] }], animations: [
trigger('fadeIn', [
transition(':enter', [style({ opacity: 0 }), animate(500)]),
]),
] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: EnjoyHintComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-enjoyhint', standalone: true, imports: [
ForceFieldComponent,
ArrowComponent,
NgClass,
DirectionPipe,
TextOrTemplateComponent,
], animations: [
trigger('fadeIn', [
transition(':enter', [style({ opacity: 0 }), animate(500)]),
]),
], providers: [provideWindow()], template: "@if (ffPositions(); as positions) {\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.top\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.left\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.right\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n<lib-force-field\n [size]=\"ffSize()\"\n [position]=\"positions.bottom\"\n [style.background-color]=\"options.backdropColor\"\n></lib-force-field>\n}\n<!-- If we just want them to click on the 'Next' button we want to disable events over the focused element -->\n<lib-force-field\n [size]=\"elementBounds()\"\n [position]=\"elementBounds()\"\n [style.background-color]=\"'transparent'\"\n [style.pointer-events]=\"step()?.event === 'next' ? 'auto' : 'none'\"\n [style.box-shadow]=\"focusElementHighlight()\"\n>\n</lib-force-field>\n\n\n<ng-template #instructions>\n <div\n class=\"instructions\"\n @fadeIn\n [ngClass]=\"{\n top: (position() | direction) === 'top',\n left: (position() | direction) === 'left',\n right: (position() | direction) === 'right',\n bottom: (position() | direction) === 'bottom',\n }\"\n [style.maxWidth]=\"instructionsWidth()\"\n >\n <lib-text-or-template [value]=\"step()?.description\">\n <ng-template let-description>\n <label [style.fontFamily]=\"options.fontFamily\">\n {{ description }}\n </label>\n </ng-template>\n </lib-text-or-template>\n @if (step()?.details) {\n <lib-text-or-template [value]=\"step()?.details\">\n <ng-template let-details>\n <p [style.fontFamily]=\"options.fontFamily\">\n {{ details }}\n </p>\n </ng-template>\n </lib-text-or-template>\n }\n @if (focusElement(); as element) {\n <lib-arrow [direction]=\"position() | direction\" [pointToElement]=\"focusElement()\"></lib-arrow>\n }\n <div class=\"buttons\">\n @if (ref.tutorial.hasPrevious() && !step()?.hidePrevious) {\n <button\n class=\"previous\"\n (click)=\"previous($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.previousButton?.className\"\n >\n {{ step()?.previousButton?.text ?? options.previousButton.text }}\n </button>\n } @if (step()?.event === 'next') {\n <button\n class=\"next\"\n (click)=\"next($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.nextButton?.className\"\n >\n {{ step()?.nextButton?.text ?? options.nextButton.text }}\n </button>\n } @if (!step()?.hideSkip) {\n <button\n class=\"skip\"\n (click)=\"skip($event)\"\n (mousedown)=\"eventBlackHole($event)\"\n [style.fontFamily]=\"options.fontFamily\"\n [ngClass]=\"step()?.skipButton?.className\"\n >\n {{ step()?.skipButton?.text ?? options.skipButton.text }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["label{color:#fff;font-size:2rem}p{color:#fff;font-size:1.5rem}.instructions{display:grid;grid-template-rows:repeat(4,auto);grid-template-columns:auto;gap:.5rem;justify-content:start;justify-items:start;padding:.75rem;max-width:25vw}.instructions label{grid-column:1;grid-row:1}.instructions p{grid-column:1;grid-row:2}.instructions lib-arrow{grid-column:1;grid-row:3}.instructions .buttons{grid-column:1;grid-row:4}.instructions.left{justify-content:end;justify-items:end}.instructions.right{justify-content:start;justify-items:start}.instructions.top{justify-content:center;justify-items:center}.instructions.top .buttons{grid-row:1}.instructions.top label{grid-row:2}.instructions.top p{grid-row:3}.instructions.top lib-arrow{grid-row:4}.instructions.bottom{justify-content:center;justify-items:center}.instructions.bottom lib-arrow{grid-row:1}.instructions.bottom label{grid-row:2}.instructions.bottom p{grid-row:3}.instructions.bottom .buttons{grid-row:4}.buttons{display:flex;flex-direction:row;gap:.5rem}.buttons button{background-color:transparent;border:1px solid white;border-radius:5px;color:#fff;font-size:.75rem;min-width:5rem;padding:.5rem;transition:all .25s ease-in-out;cursor:pointer}.buttons button.next{background-color:green;color:#fff}.buttons button:hover{background-color:#fff;color:#000}.peep-hole{box-shadow:0 0 5px 10px #fff}\n"] }]
}], ctorParameters: () => [{ type: EnjoyHintRef }, { type: Window }, { type: i2.OverlayPositionBuilder }, { type: i2.Overlay }, { type: i0.ViewContainerRef }, { type: i0.DestroyRef }, { type: i0.NgZone }], propDecorators: { instructions: [{
type: ViewChild,
args: ['instructions']
}], opacity: [{
type: HostBinding,
args: ['style.opacity']
}] } });
class Tutorial {
_stepIndex = signal(0);
_transitioning = signal(null);
stepIndex = this._stepIndex.asReadonly();
transitioning = this._transitioning.asReadonly();
step = computed(() => this.steps[this.stepIndex()]);
hasPrevious = computed(() => this.stepIndex() > 0);
steps;
constructor(steps) {
const lastStep = steps.at(-1);
if (lastStep) {
lastStep.nextButton = {
text: 'Finish',
...(lastStep.nextButton ?? {}),
};
if (lastStep.event === 'next') {
lastStep.hideSkip = lastStep.hideSkip ?? true;
}
}
this.steps = steps;
this.startHook();
}
reset() {
this._stepIndex.set(0);
}
setStepIndex(index) {
const normalizedIndex = Math.max(0, Math.min(index, this.steps.length - 1));
this._stepIndex.set(normalizedIndex);
}
async nextStep() {
await this.endHook();
this._stepIndex.update((index) => index + 1);
while (this.step() && await this.shouldSkip()) {
this._stepIndex.update((index) => index + 1);
}
await this.startHook();
}
async shouldSkip() {
// return false;
const currentStep = this.step();
if (!currentStep) {
return false;
}
const shouldSkipFn = currentStep.shouldSkip;
if (typeof shouldSkipFn !== 'function') {
return !!shouldSkipFn;
}
const result = await shouldSkipFn();
return !!result;
}
async startHook() {
const currentStep = this.step();
if (typeof currentStep?.stepStart === 'function') {
try {
this._transitioning.set('start');
await currentStep.stepStart();
}
catch (e) {
const currentStepIndex = this._stepIndex();
console.error(`Error executing stepStart hook for step ${currentStepIndex}`, e, { currentStep });
}
finally {
this._transitioning.set(null);
}
}
}
async endHook() {
const newStep = this.step();
if (typeof newStep?.stepEnd === 'function') {
try {
this._transitioning.set('end');
await newStep.stepEnd();
}
catch (e) {
const newStepIndex = this._stepIndex();
console.error(`Error executing stepEnd hook for step ${newStepIndex}`, e, { newStep });
}
finally {
this._transitioning.set(null);
}
}
}
previousStep() {
this._stepIndex.update((index) => index - 1);
}
}
class EnjoyHintService {
overlay;
elements;
originalOverflow;
/** @ignore */
constructor(overlay, elements, originalOverflow) {
this.overlay = overlay;
this.elements = elements;
this.originalOverflow = originalOverflow;
}
static inProgressTutorials = signal([]);
/** Whether there is any active tutorial */
static inProgress = computed(() => EnjoyHintService.inProgressTutorials().length > 0);
/**
* Run an interactive tutorial
* @param steps the tutorial steps to run
* @param options optional object to override the default behavior
* @returns a promise resolving when the tutorial is closed; resolves to `true` if the tutorial was completed, `false` if it was skipped
*/
async runTutorial(steps, options) {
const ref = Symbol();
EnjoyHintService.inProgressTutorials.update((ids) => [...ids, ref]);
let overlayRef = undefined;
try {
const tutorial = new Tutorial(steps);
const enjoyHintRef = new EnjoyHintRef(tutorial, options);
overlayRef = this.overlay.create({
hasBackdrop: false,
disposeOnNavigation: true,
positionStrategy: this.overlay.position().global(),
panelClass: 'ng-enjoyhint-overlay',
});
const zIndex = options?.overlayZIndex;
if (zIndex !== undefined) {
const globalOverlayWrapper = overlayRef.hostElement;
const cdkOverlayContainer = globalOverlayWrapper.parentElement;
[globalOverlayWrapper, cdkOverlayContainer].forEach((e) => e?.style.setProperty('z-index', zIndex.toString()));
}
const portal = new ComponentPortal(EnjoyHintComponent, null, Injector.create({
providers: [{ provide: EnjoyHintRef, useValue: enjoyHintRef }],
}));
this.elements.body.style.overflow = 'hidden';
overlayRef.attach(portal);
return await firstValueFrom(enjoyHintRef.onClose);
}
finally {
this.elements.body.style.overflow = this.originalOverflow;
overlayRef?.detach();
overlayRef?.dispose();
EnjoyHintService.inProgressTutorials.update((ids) => ids.filter((id) => id !== ref));
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: EnjoyHintService, deps: "invalid", target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: EnjoyHintService, providedIn: 'root', useFactory: (overlay) => new EnjoyHintService(overlay, new Elements(), getComputedStyle(document.body).overflow), deps: [{ token: Overlay }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.2", ngImport: i0, type: EnjoyHintService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
deps: [Overlay],
useFactory: (overlay) => new EnjoyHintService(overlay, new Elements(), getComputedStyle(document.body).overflow),
}]
}], ctorParameters: () => [{ type: i2.Overlay }, { type: Elements }, { type: undefined }] });
/*
* Public API Surface of ng-enjoyhint-lib
*/
/**
* Generated bundle index. Do not edit.
*/
export { EnjoyHintService };
//# sourceMappingURL=ng-enjoyhint.mjs.map