UNPKG

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
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