UNPKG

@highloop/feedback-internal

Version:

390 lines (325 loc) 9.79 kB
import mitt from 'mitt'; // @ts-ignore import App from './widget.svelte'; // @ts-ignore import ScreenshotLightbox from './screenshotLightbox.svelte'; import type { IData, ITheme } from './interfaces/data'; import { sdk } from './lib/sdk'; import type { IText } from './interfaces/text'; import { defaultText } from './config/text'; import { logError } from './lib/logError'; import { takeScreenshot } from './lib/takeScreenshot'; const FETCHED = 'fetched'; const CLOSE = 'close'; const SUBMIT = 'submit'; const RESET = 'reset'; const RENDERED = 'rendered'; const ERROR = 'error'; export let events = { FETCHED, CLOSE, SUBMIT, RESET, RENDERED, ERROR }; export class HighloopFeedbackCore { public id: string; public root: HTMLElement; public emitter: ReturnType<typeof mitt>; public isReady = false; public data?: IData; private demo?: IData; private text: IText; private themeOverride?: Partial<ITheme>; private meta?: any; private closable = false; private apiEndpoint: string; private error?: { message: string }; private app?: InstanceType<typeof App>; private updateToken?: string; private wrapper?: HTMLDivElement; private expanded?: boolean; private hideHeader?: boolean; private screenshotData?: string; private disableScreenshot?: boolean; private disablePath?: boolean; private screenshotLightbox?: InstanceType<typeof ScreenshotLightbox>; private screenshotLightboxWrapper?: HTMLElement; constructor( id: string, { root, meta, apiEndpoint, theme, demo, text, closable, expanded, hideHeader, disablePath, disableScreenshot }: { apiEndpoint?: string; theme?: Partial<ITheme>; text?: IText; demo?: IData; root: HTMLElement; meta?: any; closable?: boolean; expanded?: boolean; hideHeader?: boolean; disableScreenshot?: boolean; disablePath?: boolean; } ) { this.id = id; this.root = root; this.meta = meta; this.apiEndpoint = apiEndpoint; this.themeOverride = theme; this.text = text; this.demo = demo; this.closable = closable; this.expanded = expanded; this.hideHeader = hideHeader; this.disablePath = disablePath; this.disableScreenshot = disableScreenshot; this.emitter = mitt(); this.fetch(); } get isMounted() { return !!this.app; } private fire(type: string, value?: any) { this.emitter.emit(type, value); } public on<T>(type: string, handler: (d: T) => unknown) { this.emitter.on(type, handler); return () => this.emitter.off(type, handler); } public once<T>(type: string, handler: (d: T) => unknown) { let realHandler = (d: T) => { this.emitter.off(type, handler); handler(d); }; this.emitter.on(type, realHandler); } private fetch() { if (this.demo) { this.data = this.demo; this.isReady = true; this.fire(FETCHED, { demo: true }); } else { if (this.data) return this.fire(FETCHED, { data: this.data }); sdk.getWidget(this.id, { api: this.apiEndpoint }).then(({ data, error }) => { if (error) { logError(error); this.fire(ERROR, error); } this.data = data; this.error = error; this.isReady = true; this.fire(FETCHED, { data, error }); }); } } setMeta(meta: any) { this.meta = meta; } private ensureWrapper() { if (this.wrapper) return this.wrapper; let wrapper = document.createElement('div'); wrapper.setAttribute('data-highloop-feedback-id', this.id); wrapper.classList.add('highloop-feedback-wrapper'); this.root.appendChild(wrapper); this.wrapper = wrapper; this.updateTheme(); return wrapper; } private takeScreenshot() { if (!this.app) return; this.app.$set({ screenshotLoading: true }); takeScreenshot() .then(screenshot => { this.screenshotData = screenshot; this.app.$set({ screenshotLoading: false, screenshotData: screenshot }); }) .catch(() => { this.app.$set({ screenshotLoading: false, validationError: 'Could not take screenshot.' }); console.warn('[highloop feedback] failed to take screenshot'); }); } private closeScreenshotLightbox() { if (this.screenshotLightbox) { this.screenshotLightbox.$destroy(); this.screenshotLightbox = undefined; } if (this.screenshotLightboxWrapper) { this.screenshotLightboxWrapper.parentElement.removeChild(this.screenshotLightboxWrapper); } } private openScreenshotLightbox() { if (!this.screenshotData) return; let wrapper = document.createElement('div'); wrapper.classList.add('highloop_feedback_screenshot_preview'); this.addThemeProperties(wrapper); this.screenshotLightbox = new ScreenshotLightbox({ target: wrapper, props: { screenshot: this.screenshotData, onClose: () => this.closeScreenshotLightbox() } }); document.body.appendChild(wrapper); this.screenshotLightboxWrapper = wrapper; } render() { if (this.app) return this.update(); let wrapper = this.ensureWrapper(); this.app = new App({ target: wrapper, props: { loading: false, done: false, text: this.text || defaultText, widget: this.data?.widget, flags: { poweredBy: this.data?.flags.poweredBy, screenshot: this.data?.flags.screenshot }, disableScreenshot: this.disableScreenshot, error: this.error?.message, closable: !!this.closable, expanded: !!this.expanded, hideHeader: !!this.hideHeader, onClose: () => this.fire(CLOSE), onSubmit: (data: { stars?: number; optionId?: string; text?: string }) => { this.submitResult(data); }, onScreenshot: () => { this.takeScreenshot(); }, onShowScreenshot: () => { this.openScreenshotLightbox(); }, onClearScreenshot: () => { this.screenshotData = undefined; console.warn('[highloop feedback] cleared screenshot'); this.app.$set({ screenshotLoading: false, screenshotData: undefined }); }, onRatingOrStarsSelected: (data: { stars?: number; optionId?: string; text?: string; }) => { if (this.data.flags.autoSubmit) this.saveInternal(data); } } }); this.fire(RENDERED); if (!this.isReady) this.once(FETCHED, () => this.update()); } update() { if (!this.app) return; this.updateTheme(); this.fire(RENDERED); this.app.$set({ text: this.text || defaultText, widget: this.data?.widget, flags: this.data?.flags, error: this.error?.message, closable: !!this.closable, expanded: !!this.expanded, hideHeader: !!this.hideHeader }); } private async saveInternal( payload: | Parameters<typeof sdk.updateSubmission>[1] | Parameters<typeof sdk.createSubmission>[1] ) { if (this.demo) return Promise.resolve(); if (this.updateToken) { let res = await sdk.updateSubmission( this.id, { updateToken: this.updateToken, ...payload }, { api: this.apiEndpoint } ); if (res.error) logError(res.error); return res; } else { let res = await sdk.createSubmission( this.id, { ...payload, meta: JSON.stringify(this.meta), path: this.disablePath ? undefined : window.location.pathname.substr(0, 255) }, { api: this.apiEndpoint } ); if (res.error) logError(res.error); this.updateToken = res.data?.updateToken; return res; } } private submitResult(data: { stars?: number; optionId?: string; text?: string }) { if (!this.app) return; this.app.$set({ loading: true }); let done = () => { this.updateToken = undefined; this.app.$set({ done: true }); this.fire(SUBMIT); }; let error = () => { this.updateToken = undefined; this.app.$set({ error: 'Could not save submission' }); }; this.saveInternal({ ...data, finish: true, screenshot: this.screenshotData }).then(res => { if (res && res.error) error(); else done(); }); } reset() { if (!this.app) return; this.app.$set({ reset: true }); this.updateToken = undefined; this.fire(RESET); setTimeout(() => { this.app.$set({ done: false, loading: false }); }, 100); } destroy() { if (this.app) this.app.$destroy(); if (this.wrapper) this.root.removeChild(this.wrapper); this.closeScreenshotLightbox(); this.wrapper = undefined; this.app = undefined; } private addThemeProperties(el: HTMLElement) { let theme = Object.assign({}, this.data?.theme, this.themeOverride); for (let key in theme) { el.style.setProperty(`--loop-${key}`, theme[key]); } } private updateTheme() { if (!this.wrapper) return; this.addThemeProperties(this.wrapper); } setTheme(theme: ITheme) { this.themeOverride = theme; this.updateTheme(); } }