UNPKG

js.foresight-devtools

Version:

Visual debugging tools for ForesightJS - mouse trajectory prediction and element interaction visualization

342 lines (341 loc) 21.8 kB
import PositionObserver from '@thednp/position-observer'; import { DebuggerControlPanel } from './DebuggerControlPanel'; import { createAndAppendElement, createAndAppendStyle } from './helpers/createAndAppend'; import { updateElementOverlays } from './helpers/updateElementOverlays'; // PositionObserver imported above // Import constants that should be available from js.foresight // These constants need to be part of the main package's public API var DEFAULT_IS_DEBUGGER_MINIMIZED = false; var DEFAULT_SHOW_DEBUGGER = true; var DEFAULT_SHOW_NAME_TAGS = true; var DEFAULT_SORT_ELEMENT_LIST = 'visibility'; // Helper function that should be available from js.foresight or implemented locally function shouldUpdateSetting(newValue, currentValue) { return newValue !== undefined && newValue !== currentValue; } // Helper function that should be available from js.foresight or implemented locally function evaluateRegistrationConditions() { return { shouldRegister: typeof window !== 'undefined' && !('ontouchstart' in window), }; } var ForesightDebugger = /** @class */ (function () { function ForesightDebugger(foresightManager) { var _this = this; this.callbackAnimations = new Map(); this._debuggerSettings = { showDebugger: DEFAULT_SHOW_DEBUGGER, isControlPanelDefaultMinimized: DEFAULT_IS_DEBUGGER_MINIMIZED, showNameTags: DEFAULT_SHOW_NAME_TAGS, sortElementList: DEFAULT_SORT_ELEMENT_LIST, }; this.debugElementOverlays = new Map(); this.predictedMouseIndicator = null; this.mouseTrajectoryLine = null; this.scrollTrajectoryLine = null; this.managerSubscriptionsController = null; this.animationPositionObserver = null; this.handleAnimationPositionChange = function (entries) { for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { var entry = entries_1[_i]; var animationData = _this.callbackAnimations.get(entry.target); if (animationData) { var rect = entry.boundingClientRect; var hitSlop = animationData.hitSlop, overlay = animationData.overlay; var newLeft = rect.left - hitSlop.left; var newTop = rect.top - hitSlop.top; var newWidth = rect.width + hitSlop.left + hitSlop.right; var newHeight = rect.height + hitSlop.top + hitSlop.bottom; overlay.style.transform = "translate3d(".concat(newLeft, "px, ").concat(newTop, "px, 0)"); overlay.style.width = "".concat(newWidth, "px"); overlay.style.height = "".concat(newHeight, "px"); } } }; this.handleElementDataUpdated = function (e) { var _a; switch (e.updatedProp) { case 'bounds': _this.createOrUpdateElementOverlay(e.elementData); break; case 'visibility': if (!e.elementData.isIntersectingWithViewport) { _this.removeElementOverlay(e.elementData); } (_a = _this.controlPanel) === null || _a === void 0 ? void 0 : _a.updateElementVisibilityStatus(e.elementData); break; } }; /** * Removes all debug overlays and data associated with an element. * * This method cleans up the link overlay, expanded overlay, and name label * for the specified element, removes it from internal tracking maps, and * refreshes the control panel's element list to reflect the removal. * * @param element - The ForesightElement to remove from debugging visualization */ this.handleRemoveElement = function (e) { var _a; (_a = _this.controlPanel) === null || _a === void 0 ? void 0 : _a.removeElementFromList(e.elementData); _this.removeElementOverlay(e.elementData); }; this.handleCallbackFired = function (e) { _this.showCallbackAnimation(e.elementData); }; this.handleAddElement = function (e) { _this.createOrUpdateElementOverlay(e.elementData); _this.controlPanel.addElementToList(e.elementData, e.sort); }; this.handleMouseTrajectoryUpdate = function (e) { if (!_this.shadowRoot || !_this.debugContainer) { return; } if (!_this.predictedMouseIndicator || !_this.mouseTrajectoryLine) { return; } //Hide scroll visuals on mouse move if (_this.scrollTrajectoryLine) { _this.scrollTrajectoryLine.style.display = 'none'; } var _a = e.trajectoryPositions, predictedPoint = _a.predictedPoint, currentPoint = _a.currentPoint; // Use transform for positioning to avoid layout reflow. // The CSS handles centering the element with `translate(-50%, -50%)`. _this.predictedMouseIndicator.style.transform = "translate3d(".concat(predictedPoint.x, "px, ").concat(predictedPoint.y, "px, 0) translate3d(-50%, -50%, 0)"); _this.predictedMouseIndicator.style.display = e.predictionEnabled ? 'block' : 'none'; // This hides the circle from the UI at the top-left corner when refreshing the page with the cursor outside of the window if (predictedPoint.x === 0 && predictedPoint.y === 0) { _this.predictedMouseIndicator.style.display = 'none'; return; } if (!e.predictionEnabled) { _this.mouseTrajectoryLine.style.display = 'none'; return; } var dx = predictedPoint.x - currentPoint.x; var dy = predictedPoint.y - currentPoint.y; var length = Math.sqrt(dx * dx + dy * dy); var angle = (Math.atan2(dy, dx) * 180) / Math.PI; // Use a single transform to position, rotate, and scale the line, // avoiding reflow from top/left changes. _this.mouseTrajectoryLine.style.transform = "translate3d(".concat(currentPoint.x, "px, ").concat(currentPoint.y, "px, 0) rotate(").concat(angle, "deg)"); _this.mouseTrajectoryLine.style.width = "".concat(length, "px"); _this.mouseTrajectoryLine.style.display = 'block'; }; this.handleScrollTrajectoryUpdate = function (e) { if (!_this.scrollTrajectoryLine) return; var dx = e.predictedPoint.x - e.currentPoint.x; var dy = e.predictedPoint.y - e.currentPoint.y; var length = Math.sqrt(dx * dx + dy * dy); var angle = (Math.atan2(dy, dx) * 180) / Math.PI; _this.scrollTrajectoryLine.style.transform = "translate3d(".concat(e.currentPoint.x, "px, ").concat(e.currentPoint.y, "px, 0) rotate(").concat(angle, "deg)"); _this.scrollTrajectoryLine.style.width = "".concat(length, "px"); _this.scrollTrajectoryLine.style.display = 'block'; }; this.handleSettingsChanged = function (e) { var _a; (_a = _this.controlPanel) === null || _a === void 0 ? void 0 : _a.updateControlsState(e.newSettings, _this._debuggerSettings); }; this.foresightManagerInstance = foresightManager; } Object.defineProperty(ForesightDebugger.prototype, "getDebuggerData", { get: function () { return { settings: this._debuggerSettings, }; }, enumerable: false, configurable: true }); ForesightDebugger.initialize = function (foresightManager, props) { if (typeof window === 'undefined' || !evaluateRegistrationConditions().shouldRegister) { return null; } if (!ForesightDebugger.isInitiated) { ForesightDebugger.debuggerInstance = new ForesightDebugger(foresightManager); } var instance = ForesightDebugger.debuggerInstance; instance.subscribeToManagerEvents(); instance.alterDebuggerSettings(props); // Always call at the end of the initialize function if (!instance.shadowHost) { instance._setupDOM(); } return instance; }; Object.defineProperty(ForesightDebugger, "instance", { get: function () { if (!ForesightDebugger.debuggerInstance) { throw new Error('ForesightDebugger has not been initialized. Call ForesightDebugger.initialize() first.'); } return ForesightDebugger.debuggerInstance; }, enumerable: false, configurable: true }); ForesightDebugger.prototype._setupDOM = function () { // If for some reason we call this on an already-setup instance, do nothing. if (this.shadowHost) { return; } this.shadowHost = createAndAppendElement('div', document.body, { id: 'jsforesight-debugger-shadow-host', }); this.shadowRoot = this.shadowHost.attachShadow({ mode: 'open' }); this.debugContainer = createAndAppendElement('div', this.shadowRoot, { id: 'jsforesight-debug-container', }); this.predictedMouseIndicator = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-mouse-predicted', }); this.mouseTrajectoryLine = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-trajectory-line', }); this.scrollTrajectoryLine = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-scroll-trajectory-line', }); this.controlPanel = DebuggerControlPanel.initialize(this.foresightManagerInstance, ForesightDebugger.debuggerInstance, this.shadowRoot, this._debuggerSettings); createAndAppendStyle(debuggerCSS, this.shadowRoot, 'screen-visuals'); this.animationPositionObserver = new PositionObserver(this.handleAnimationPositionChange); }; Object.defineProperty(ForesightDebugger, "isInitiated", { get: function () { return !!ForesightDebugger.debuggerInstance; }, enumerable: false, configurable: true }); ForesightDebugger.prototype.alterDebuggerSettings = function (props) { if (shouldUpdateSetting(props === null || props === void 0 ? void 0 : props.showNameTags, this._debuggerSettings.showNameTags)) { this._debuggerSettings.showNameTags = props.showNameTags; this.toggleNameTagVisibility(); } if (shouldUpdateSetting(props === null || props === void 0 ? void 0 : props.isControlPanelDefaultMinimized, this._debuggerSettings.isControlPanelDefaultMinimized)) { this._debuggerSettings.isControlPanelDefaultMinimized = props.isControlPanelDefaultMinimized; } if (shouldUpdateSetting(props === null || props === void 0 ? void 0 : props.sortElementList, this._debuggerSettings.sortElementList)) { this._debuggerSettings.sortElementList = props.sortElementList; } if (shouldUpdateSetting(props === null || props === void 0 ? void 0 : props.showDebugger, this._debuggerSettings.showDebugger)) { this._debuggerSettings.showDebugger = props.showDebugger; if (this._debuggerSettings.showDebugger) { ForesightDebugger.initialize(this.foresightManagerInstance); } else { this.cleanup(); } } }; ForesightDebugger.prototype.subscribeToManagerEvents = function () { this.managerSubscriptionsController = new AbortController(); var signal = this.managerSubscriptionsController.signal; var manager = this.foresightManagerInstance; manager.addEventListener('elementRegistered', this.handleAddElement, { signal: signal }); manager.addEventListener('elementUnregistered', this.handleRemoveElement, { signal: signal }); manager.addEventListener('elementDataUpdated', this.handleElementDataUpdated, { signal: signal }); manager.addEventListener('mouseTrajectoryUpdate', this.handleMouseTrajectoryUpdate, { signal: signal, }); manager.addEventListener('scrollTrajectoryUpdate', this.handleScrollTrajectoryUpdate, { signal: signal, }); manager.addEventListener('managerSettingsChanged', this.handleSettingsChanged, { signal: signal }); manager.addEventListener('callbackFired', this.handleCallbackFired, { signal: signal }); }; ForesightDebugger.prototype.createElementOverlays = function (elementData) { var expandedOverlay = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-expanded-overlay', data: elementData.name, }); var nameLabel = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-name-label', }); var overlays = { expandedOverlay: expandedOverlay, nameLabel: nameLabel }; this.debugElementOverlays.set(elementData.element, overlays); return overlays; }; ForesightDebugger.prototype.createOrUpdateElementOverlay = function (newData) { var _a; if (!this.debugContainer || !this.shadowRoot) return; var overlays = this.debugElementOverlays.get(newData.element); if (!overlays) { overlays = this.createElementOverlays(newData); } updateElementOverlays(overlays, newData, (_a = this._debuggerSettings.showNameTags) !== null && _a !== void 0 ? _a : DEFAULT_SHOW_NAME_TAGS); }; // TODO :fix ForesightDebugger.prototype.toggleNameTagVisibility = function () { var _this = this; this.foresightManagerInstance.registeredElements.forEach(function (elementData) { var _a; var overlays = _this.debugElementOverlays.get(elementData.element); if (!overlays) return; updateElementOverlays(overlays, elementData, (_a = _this._debuggerSettings.showNameTags) !== null && _a !== void 0 ? _a : DEFAULT_SHOW_NAME_TAGS); }); }; ForesightDebugger.prototype.removeElementOverlay = function (elementData) { var overlays = this.debugElementOverlays.get(elementData.element); if (overlays) { overlays.expandedOverlay.remove(); overlays.nameLabel.remove(); this.debugElementOverlays.delete(elementData.element); } }; ForesightDebugger.prototype.showCallbackAnimation = function (elementData) { var _this = this; var _a, _b; var element = elementData.element, elementBounds = elementData.elementBounds; var existingAnimation = this.callbackAnimations.get(element); // If an animation is already running for this element, reset it if (existingAnimation) { clearTimeout(existingAnimation.timeoutId); existingAnimation.overlay.remove(); (_a = this.animationPositionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(element); this.callbackAnimations.delete(element); } var animationOverlay = createAndAppendElement('div', this.debugContainer, { className: 'jsforesight-callback-indicator', }); var _c = elementBounds.expandedRect, left = _c.left, top = _c.top, right = _c.right, bottom = _c.bottom; var width = right - left; var height = bottom - top; animationOverlay.style.display = 'block'; animationOverlay.style.transform = "translate3d(".concat(left, "px, ").concat(top, "px, 0)"); animationOverlay.style.width = "".concat(width, "px"); animationOverlay.style.height = "".concat(height, "px"); animationOverlay.classList.add('animate'); var animationDuration = 500; var timeoutId = setTimeout(function () { var _a; animationOverlay.remove(); _this.callbackAnimations.delete(element); (_a = _this.animationPositionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(element); }, animationDuration); this.callbackAnimations.set(element, { hitSlop: elementData.elementBounds.hitSlop, overlay: animationOverlay, timeoutId: timeoutId, }); (_b = this.animationPositionObserver) === null || _b === void 0 ? void 0 : _b.observe(element); }; ForesightDebugger.prototype.cleanup = function () { var _a, _b, _c; (_a = this.managerSubscriptionsController) === null || _a === void 0 ? void 0 : _a.abort(); (_b = this.controlPanel) === null || _b === void 0 ? void 0 : _b.cleanup(); (_c = this.shadowHost) === null || _c === void 0 ? void 0 : _c.remove(); this.debugElementOverlays.clear(); this.shadowHost = null; this.shadowRoot = null; this.debugContainer = null; this.predictedMouseIndicator = null; this.mouseTrajectoryLine = null; this.scrollTrajectoryLine = null; this.controlPanel = null; }; return ForesightDebugger; }()); export { ForesightDebugger }; var debuggerCSS = /* css */ "\n #jsforesight-debug-container { \n position: fixed; top: 0; left: 0; width: 100%; height: 100%;\n pointer-events: none; z-index: 9999;\n }\n\n .jsforesight-expanded-overlay, \n .jsforesight-name-label, \n .jsforesight-callback-indicator,\n .jsforesight-mouse-predicted,\n .jsforesight-scroll-trajectory-line,\n .jsforesight-trajectory-line {\n position: absolute;\n top: 0;\n left: 0;\n will-change: transform; \n }\n .jsforesight-trajectory-line{\n display: none;\n }\n .jsforesight-expanded-overlay {\n border: 1px dashed rgba(100, 116, 139, 0.4);\n background-color: rgba(100, 116, 139, 0.05);\n box-sizing: border-box;\n border-radius: 8px;\n }\n .jsforesight-mouse-predicted {\n display: none !important;\n /* transform is now set dynamically via JS for performance */\n }\n .jsforesight-trajectory-line {\n height: 4px;\n background: linear-gradient(90deg, #3b82f6, rgba(59, 130, 246, 0.4));\n transform-origin: left center;\n z-index: 9999;\n border-radius: 2px;\n box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);\n position: relative;\n /* width and transform are set dynamically via JS for performance */\n }\n .jsforesight-trajectory-line::after {\n content: '';\n position: absolute;\n right: -6px;\n top: 50%;\n transform: translateY(-50%);\n width: 0;\n height: 0;\n border-left: 8px solid #3b82f6;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n filter: drop-shadow(0 0 6px rgba(59, 130, 246, 0.6));\n }\n .jsforesight-name-label {\n background-color: rgba(27, 31, 35, 0.85);\n backdrop-filter: blur(4px);\n color: white;\n padding: 4px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\";\n border-radius: 4px;\n z-index: 10001;\n white-space: nowrap;\n pointer-events: none;\n }\n .jsforesight-callback-indicator {\n border: 4px solid oklch(65% 0.22 280); \n border-radius: 8px;\n box-sizing: border-box;\n pointer-events: none;\n opacity: 0;\n z-index: 10002;\n display: none; \n }\n .jsforesight-callback-indicator.animate {\n animation: jsforesight-callback-pulse 0.5s ease-out forwards;\n }\n \n .jsforesight-scroll-trajectory-line {\n height: 4px;\n background: repeating-linear-gradient(\n 90deg,\n #22c55e 0px,\n #22c55e 8px,\n transparent 8px,\n transparent 16px\n );\n transform-origin: left center;\n z-index: 9999;\n border-radius: 2px;\n display: none;\n animation: scroll-dash-flow 1.5s linear infinite;\n position: relative;\n box-shadow: 0 0 12px rgba(34, 197, 94, 0.4);\n }\n\n .jsforesight-scroll-trajectory-line::after {\n content: '';\n position: absolute;\n right: -6px;\n top: 50%;\n transform: translateY(-50%);\n width: 0;\n height: 0;\n border-left: 8px solid #22c55e;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n filter: drop-shadow(0 0 6px rgba(34, 197, 94, 0.6));\n animation: scroll-arrow-pulse 1.5s ease-in-out infinite;\n }\n\n @keyframes scroll-dash-flow {\n 0% { background-position: 0px 0px; }\n 100% { background-position: 16px 0px; }\n }\n\n @keyframes scroll-arrow-pulse {\n 0%, 100% { \n transform: translateY(-50%) scale(1);\n filter: drop-shadow(0 0 6px rgba(34, 197, 94, 0.6));\n }\n 50% {\n transform: translateY(-50%) scale(1.2);\n filter: drop-shadow(0 0 12px rgba(34, 197, 94, 0.8));\n }\n }\n\n\n \n @keyframes jsforesight-callback-pulse {\n 0% {\n opacity: 1;\n box-shadow: 0 0 15px oklch(65% 0.22 280 / 0.7);\n }\n 100% {\n opacity: 0;\n box-shadow: 0 0 25px oklch(65% 0.22 280 / 0);\n }\n }\n ";