UNPKG

@multiplayer-app/session-recorder-browser

Version:
659 lines 25.9 kB
import { Observable } from 'lib0/observable'; import { insertTrustedHTML, injectStylesIntoShadowRoot, formatTimeForSessionTimer } from '../utils'; import { SessionState } from '../types'; import { POPOVER_WIDTH, NON_DRAGGABLE_OFFSET, POPOVER_DISTANCE_FROM_BUTTON, } from './constants'; import { DEFAULT_WIDGET_TEXT_CONFIG } from '../config'; import { isBrowser, isBrowserExtension } from '../global'; import { UIManager } from './UIManager'; import { DragManager } from './dragManager'; import { ButtonState, buttonStates, continuousRecordingSaveButtonStates, } from './buttonStateConfigs'; // Import styles as string for shadow DOM injection import widgetStyles from './styles/index.scss?raw'; import './styles/button.scss'; export class SessionWidget extends Observable { set buttonState(newState) { var _a, _b; this._buttonState = newState; if (!this.isBrowser) return; const { icon, tooltip, classes, excludeClasses } = buttonStates[newState]; if (newState === ButtonState.CANCEL) { (_a = this.buttonDraggabilityObserver) === null || _a === void 0 ? void 0 : _a.observe(this.recorderButton, { attributes: true, attributeOldValue: true, attributeFilter: ['class'], }); } else { (_b = this.buttonDraggabilityObserver) === null || _b === void 0 ? void 0 : _b.disconnect(); } this.uiManager.setPopoverLoadingState(newState === ButtonState.LOADING); this.updateButton(icon, tooltip, excludeClasses, classes); } set initialPopoverVisible(v) { var _a; this._initialPopoverVisible = v; if (this.isBrowser) { (_a = this.initialPopover) === null || _a === void 0 ? void 0 : _a.classList.toggle('hidden', !v); } } set finalPopoverVisible(v) { var _a; this._finalPopoverVisible = v; if (this.isBrowser) { (_a = this.finalPopover) === null || _a === void 0 ? void 0 : _a.classList.toggle('hidden', !v); } } get error() { return this._error; } set error(v) { this._error = v; if (this._error) { this.showToast({ type: 'error', message: this._error, button: { text: 'Close', onClick: () => this.hideToast(), }, }); } } set isStarted(v) { this._isStarted = v; if (!this.isBrowser) return; if (this.isBrowserExtension && v && !this._continuousRecording) { this.overlay.classList.remove('hidden'); this.makeOverlayDraggable(); if (!this.seconds) { this.startTimer(); } } else { this.overlay.classList.add('hidden'); } this.recorderButton.classList.toggle('is-started', this._isStarted); if (this._isStarted) { if (!this._continuousRecording) { this.initialPopoverVisible = false; this.buttonState = ButtonState.RECORDING; } else { this.buttonState = ButtonState.CONTINUOUS_DEBUGGING; } } else { this.buttonState = ButtonState.IDLE; } } set isPaused(v) { this._isPaused = v; if (!this.isBrowser) return; if (this._isInitialized && this.isBrowserExtension && v && !this._continuousRecording) { this.overlay.classList.add('hidden'); this.submitSessionDialog.classList.remove('hidden'); this.stopTimer(); } } constructor() { super(); this._isStarted = false; this._isPaused = false; this._isInitialized = false; this._error = ''; this._recorderPlacement = ''; this._showWidget = false; this._initialPopoverVisible = false; this._finalPopoverVisible = false; this._buttonState = ButtonState.IDLE; this._continuousRecording = false; this._showContinuousRecording = true; this._widgetTextOverrides = DEFAULT_WIDGET_TEXT_CONFIG; this.commentTextarea = null; this.dragManager = null; this.buttonClickExternalHandler = null; this.seconds = 0; this.shadowRoot = null; this.hostElement = null; this.handleClickOutside = (event) => { var _a; const target = event.target; const isPopoverVisible = this._initialPopoverVisible || this._finalPopoverVisible; const popover = this._initialPopoverVisible ? this.initialPopover : this.finalPopover; if (!isPopoverVisible || !target) return; if (target !== this.hostElement && !(popover === null || popover === void 0 ? void 0 : popover.contains(target)) && !((_a = this.recorderButton) === null || _a === void 0 ? void 0 : _a.contains(target)) && target !== this.recorderButton && target !== popover) { if (this._initialPopoverVisible) { this.handleCloseInitialPopover(); } else { this.handleCloseFinalPopover(); } } }; this.isBrowser = isBrowser; this.isBrowserExtension = isBrowserExtension; if (!this.isBrowser) { // Create dummy elements for SSR to prevent crashes this.uiManager = {}; this.toast = {}; this.overlay = {}; this.finalPopover = {}; this.initialPopover = {}; this.submitSessionDialog = {}; this.recorderButton = {}; return; } this.toast = document.createElement('div'); this.overlay = document.createElement('div'); this.finalPopover = document.createElement('div'); this.initialPopover = document.createElement('div'); this.recorderButton = document.createElement('button'); this.submitSessionDialog = document.createElement('div'); this.uiManager = new UIManager(this.recorderButton, this.initialPopover, this.finalPopover, this.overlay, this.submitSessionDialog, this.toast, DEFAULT_WIDGET_TEXT_CONFIG, true); this.uiManager.setRecorderButtonProps(); this.uiManager.setInitialPopoverProps(); this.uiManager.setFinalPopoverProps(); this.uiManager.setOverlayProps(); this.uiManager.setSubmitSessionDialogProps(); this.uiManager.setToastProps(); this.commentTextarea = this.finalPopover.querySelector('.mp-session-debugger-popover-textarea'); this.observeButtonDraggableMode(); } updateState(state, continuousRecording) { this._continuousRecording = continuousRecording; switch (state) { case SessionState.started: this.isPaused = false; this.isStarted = true; break; case SessionState.stopped: this.isPaused = false; this.isStarted = false; break; case SessionState.paused: this.isPaused = true; this.isStarted = false; break; default: this.isPaused = false; this.isStarted = false; break; } } updateContinuousRecordingState(checked, disabled = false) { if (!this.isBrowser) return; const toggleCheckbox = this.initialPopover.querySelector('#mp-session-debugger-continuous-debugging-checkbox'); if (toggleCheckbox) { toggleCheckbox.checked = checked; toggleCheckbox.disabled = disabled; } } updateSaveContinuousDebugSessionState(state) { if (!this.isBrowser) return; const saveButton = this.initialPopover.querySelector('#mp-save-continuous-debug-session'); if (saveButton) { const { textContent, disabled } = continuousRecordingSaveButtonStates[state]; saveButton.disabled = disabled; saveButton.textContent = textContent; } } /** * Shows a toast message with optional action button * @param config - The toast configuration including message, type, and optional button * @param duration - Duration in milliseconds to show the toast (default: 10000ms) */ showToast(config, duration = 10000) { if (!this.isBrowser) return; this.uiManager.showToast(config, duration); } /** * Hides the currently displayed toast message */ hideToast() { if (!this.isBrowser) return; this.uiManager.hideToast(); } observeButtonDraggableMode() { if (!this.isBrowser) return; this.buttonDraggabilityObserver = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const oldClassName = mutation.oldValue; const newClassName = mutation.target['className']; if (((oldClassName === null || oldClassName === void 0 ? void 0 : oldClassName.includes('no-draggable')) && !newClassName.includes('no-draggable')) || ((newClassName === null || newClassName === void 0 ? void 0 : newClassName.includes('no-draggable')) && !(oldClassName === null || oldClassName === void 0 ? void 0 : oldClassName.includes('no-draggable')))) { // draggable mode was changed this.initialPopoverVisible = false; this.finalPopoverVisible = false; } } } }); } init(options) { if (this._isInitialized) return; if (!this.isBrowser) return; this._isInitialized = true; this._showWidget = options.showWidget; this._showContinuousRecording = options.showContinuousRecording; this._widgetTextOverrides = { ...this._widgetTextOverrides, ...options.widgetTextOverrides, }; // Recreate UIManager with proper config this.uiManager = new UIManager(this.recorderButton, this.initialPopover, this.finalPopover, this.overlay, this.submitSessionDialog, this.toast, this._widgetTextOverrides, this._showContinuousRecording); // Re-initialize templates with new config this.uiManager.setRecorderButtonProps(); this.uiManager.setInitialPopoverProps(); this.uiManager.setFinalPopoverProps(); this.uiManager.setOverlayProps(); this.uiManager.setSubmitSessionDialogProps(); this.uiManager.setToastProps(); const elements = [this.toast]; if (options.showWidget) { elements.push(this.recorderButton, this.initialPopover, this.finalPopover, this.submitSessionDialog); } else { elements.push(this.overlay, this.submitSessionDialog); } this.appendElements(elements); // Hide continuous recording UI when feature is disabled if (!this._showContinuousRecording) { const cont = this.initialPopover.querySelector('.mp-session-debugger-continuous-debugging'); cont && cont.classList.add('hidden'); const overlay = this.initialPopover.querySelector('.mp-session-debugger-continuous-debugging-overlay'); overlay && overlay.classList.add('hidden'); } if (options.showWidget && options.widgetButtonPlacement) { this.recorderButton.classList.add(options.widgetButtonPlacement); this._recorderPlacement = options.widgetButtonPlacement; this.addRecorderDragFunctionality(); } this.addEventListeners(); } appendElements(elements) { if (!this.isBrowser || typeof document === 'undefined') return; // Create host element const rootWrapper = document.createElement('mp-root'); rootWrapper.classList.add('mp-root-wrapper'); rootWrapper.setAttribute('data-rr-ignore', 'true'); this.hostElement = rootWrapper; // Create shadow root this.shadowRoot = rootWrapper.attachShadow({ mode: 'open' }); // Inject styles directly from imported SCSS file if (widgetStyles) { injectStylesIntoShadowRoot(this.shadowRoot, widgetStyles); } // Append all elements to shadow root elements.forEach((element) => this.shadowRoot.appendChild(element)); // Append host element to document body document.body.appendChild(rootWrapper); } addRecorderDragFunctionality() { if (!this.isBrowser) return; this.dragManager = new DragManager(this.recorderButton, this._recorderPlacement, () => { if (this._isPaused) { this.finalPopoverVisible = true; } }, () => this.updatePopoverPosition(), (e) => this.onRecordingButtonClick(e)); this.dragManager.init(); } updatePopoverPosition() { if (!this.isBrowser || typeof window === 'undefined') return; const { top, right, bottom, left } = this.recorderButton.getBoundingClientRect(); const isDraggable = !this.recorderButton.classList.contains('no-draggable'); const POPOVER_HEIGHT = this._isStarted ? 400 : 300; const VIEWPORT_WIDTH = window.innerWidth; const VIEWPORT_HEIGHT = window.innerHeight; let popoverBottom; let popoverRight; popoverBottom = VIEWPORT_HEIGHT - top + POPOVER_DISTANCE_FROM_BUTTON + (isDraggable ? 0 : NON_DRAGGABLE_OFFSET); popoverRight = VIEWPORT_WIDTH - right; if (popoverBottom + POPOVER_HEIGHT > VIEWPORT_HEIGHT) { popoverBottom = VIEWPORT_HEIGHT - bottom - POPOVER_HEIGHT - POPOVER_DISTANCE_FROM_BUTTON - (isDraggable ? 0 : NON_DRAGGABLE_OFFSET); } if (popoverRight + POPOVER_WIDTH > VIEWPORT_WIDTH) { popoverRight = VIEWPORT_WIDTH - left - POPOVER_WIDTH; } const MIN_MARGIN = 10; popoverBottom = Math.max(popoverBottom, MIN_MARGIN); popoverRight = Math.max(popoverRight, MIN_MARGIN); if (popoverRight + POPOVER_WIDTH > VIEWPORT_WIDTH - MIN_MARGIN) { popoverRight = VIEWPORT_WIDTH - POPOVER_WIDTH - MIN_MARGIN; } if (popoverBottom + POPOVER_HEIGHT > VIEWPORT_HEIGHT - MIN_MARGIN) { popoverBottom = VIEWPORT_HEIGHT - POPOVER_HEIGHT - MIN_MARGIN; } const updatePopoverStyles = (popover) => { popover.style.right = `${popoverRight}px`; popover.style.bottom = `${popoverBottom}px`; popover.style.left = 'unset'; popover.style.top = 'unset'; }; requestAnimationFrame(() => { this.initialPopover && updatePopoverStyles(this.initialPopover); this.finalPopover && updatePopoverStyles(this.finalPopover); }); } addEventListeners() { if (!this.isBrowser) return; const events = []; if (this.isBrowserExtension) { events.push({ target: this.overlay, selector: '.mp-stop-btn', handler: this.onPause.bind(this), // change to submit dialog }, { target: this.submitSessionDialog, selector: '#mp-submit-recording', handler: this.onStop.bind(this), }, { target: this.submitSessionDialog, selector: '#mp-cancel-submission', handler: this.onCancel.bind(this), }); } if (this._showWidget) { events.push({ target: this.initialPopover, selector: '.mp-start-recording', handler: this.startRecording.bind(this), }); if (this._showContinuousRecording) { events.push({ event: 'change', target: this.initialPopover, selector: '#mp-session-debugger-continuous-debugging-checkbox', handler: this.handleContinuousRecordingChange.bind(this), }, { target: this.initialPopover, selector: '#mp-save-continuous-debug-session', handler: this.handleSaveContinuousDebugSession.bind(this), }); } events.push({ target: this.initialPopover, selector: '.mp-session-debugger-modal-close', handler: this.handleCloseInitialPopover.bind(this), }, { target: this.finalPopover, selector: '.mp-stop-recording', handler: this.handleStopRecording.bind(this), }, { target: this.finalPopover, selector: '.mp-session-debugger-dismiss-button', handler: this.handleDismissRecording.bind(this), }, { target: this.finalPopover, selector: '.mp-session-debugger-modal-close', handler: this.handleCloseFinalPopover.bind(this), }); } events.forEach(({ target, selector, handler, event = 'click' }) => { this.addListener(target, selector, handler, event); }); } handleStopRecording() { if (!this.isBrowser) return; this.onStop(); this.handleUIReseting(); } handleUIReseting() { if (!this.isBrowser) return; this.finalPopoverVisible = false; this.resetRecordingButton(); } handleCloseInitialPopover() { if (!this.isBrowser) return; if (this._buttonState === ButtonState.LOADING) { this.onCancel(); } this.initialPopoverVisible = false; this.buttonState = this._continuousRecording ? ButtonState.CONTINUOUS_DEBUGGING : ButtonState.IDLE; if (typeof document !== 'undefined') { document.removeEventListener('click', this.handleClickOutside); } } handleCloseFinalPopover() { this.onResume(); } onRequestError() { if (!this.isBrowser) return; this.initialPopoverVisible = false; this.finalPopoverVisible = false; this.buttonState = ButtonState.IDLE; if (typeof document !== 'undefined') { document.removeEventListener('click', this.handleClickOutside); } } handleDismissRecording() { if (!this.isBrowser) return; this.onCancel(); this.finalPopoverVisible = !this._finalPopoverVisible; this.buttonState = ButtonState.IDLE; this.overlay.classList.add('hidden'); if (this.commentTextarea) { this.commentTextarea.value = ''; } } resetRecordingButton() { setTimeout(() => { this.buttonState = ButtonState.IDLE; }, 1500); } addListener(element, selector, handler, event = 'click') { var _a; (_a = element === null || element === void 0 ? void 0 : element.querySelector(selector)) === null || _a === void 0 ? void 0 : _a.addEventListener(event, handler); } onRecordingButtonClick(e) { if (!this.isBrowser) return; if (this.buttonClickExternalHandler) { const shouldPropagate = this.buttonClickExternalHandler(); if (shouldPropagate === false) { e.preventDefault(); return; } } if (this._initialPopoverVisible) { this.handleCloseInitialPopover(); return; } if (this._isPaused) { this.onResume(); return; } if (this._isStarted) { this.buttonState = ButtonState.CANCEL; if (this._continuousRecording) { this.initialPopoverVisible = !this.initialPopoverVisible; } else { this.finalPopoverVisible = !this._finalPopoverVisible; this.onPause(); } } else { this.buttonState = this._initialPopoverVisible ? ButtonState.IDLE : ButtonState.CANCEL; this.initialPopoverVisible = !this._initialPopoverVisible; } if (typeof document !== 'undefined') { if (this._initialPopoverVisible || this._finalPopoverVisible) { document.addEventListener('click', this.handleClickOutside); } else { document.removeEventListener('click', this.handleClickOutside); } } } updateButton(innerHTML, tooltip, excludeClasses, classes) { if (!this.isBrowser || !this.recorderButton) return; insertTrustedHTML(this.recorderButton, `${innerHTML}`); this.recorderButton.dataset['tooltip'] = tooltip; if (excludeClasses) { this.recorderButton.classList.remove(...excludeClasses); } if (classes) { this.recorderButton.classList.add(...classes); } } handleContinuousRecordingChange(e) { if (!this._showContinuousRecording) return; const checkbox = e.target; this.emit('continuous-debugging', [checkbox.checked]); } handleSaveContinuousDebugSession() { this.emit('save', []); } startRecording() { this.buttonState = ButtonState.LOADING; this.onStart(); } onStart() { if (!this.recorderButton) return; this.emit('start', []); } onStop() { if (!this.isBrowser) return; if (this._showWidget && !this.recorderButton) return; let commentElement = null; if (this.isBrowserExtension) { this.submitSessionDialog.classList.add('hidden'); commentElement = this.submitSessionDialog.querySelector('#mp-recording-comment'); } else { commentElement = this.commentTextarea; } if (commentElement) { this.emit('stop', [commentElement.value]); commentElement.value = ''; } else { this.emit('stop', []); } } onPause() { this.emit('pause', []); } onResume() { if (!this.isBrowser) return; this.finalPopoverVisible = false; if (!this._continuousRecording) { this.buttonState = ButtonState.RECORDING; this.emit('resume', []); } else { this.buttonState = ButtonState.CONTINUOUS_DEBUGGING; } } onCancel() { if (!this.isBrowser) return; this.submitSessionDialog.classList.add('hidden'); this.emit('cancel', []); } enable() { if (!this.isBrowser || !this.recorderButton) return; this.recorderButton.disabled = false; this.recorderButton.style.opacity = '1'; } disable() { if (!this.isBrowser || !this.recorderButton) return; this.recorderButton.disabled = true; this.recorderButton.style.opacity = '0.5'; } destroy() { if (!this.isBrowser || typeof document === 'undefined') return; const rootWrapper = this.hostElement || document.querySelector('.mp-root-wrapper'); if (rootWrapper && rootWrapper.parentNode) { rootWrapper.parentNode.removeChild(rootWrapper); } this.shadowRoot = null; this.hostElement = null; document.removeEventListener('click', this.handleClickOutside); } startTimer() { if (!this.isBrowser) return; if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } this.uiManager.setTimerValue(formatTimeForSessionTimer(this.seconds)); this.timerInterval = setInterval(() => { this.seconds++; this.uiManager.setTimerValue(formatTimeForSessionTimer(this.seconds)); }, 1000); } stopTimer() { if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; this.seconds = 0; } } makeOverlayDraggable() { if (!this.isBrowser || typeof document === 'undefined') return; const element = this.overlay; const dragHandle = element.querySelector('.mp-drag-handle'); if (!dragHandle) return; let offsetX = 0, offsetY = 0; dragHandle.onmousedown = function (e) { e.preventDefault(); offsetX = e.clientX - element.offsetLeft; offsetY = e.clientY - element.offsetTop; document.onmousemove = function (e) { element.style.left = `${e.clientX - offsetX}px`; element.style.top = `${e.clientY - offsetY}px`; element.style.bottom = 'auto'; element.style.transform = 'none'; }; document.onmouseup = function () { document.onmousemove = null; document.onmouseup = null; }; }; } } //# sourceMappingURL=index.js.map