js.foresight-devtools
Version:
Visual debugging tools for ForesightJS - mouse trajectory prediction and element interaction visualization
342 lines (341 loc) • 21.8 kB
JavaScript
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 ";