UNPKG

@teachinglab/omd

Version:

omd

201 lines (168 loc) 7.44 kB
import { omdEquationNode } from '../nodes/omdEquationNode.js'; import { omdStepVisualizerInteractiveSteps } from '../utils/omdStepVisualizerInteractiveSteps.js'; /** * Manages interactive step text boxes that appear when dots are clicked * Handles creation, positioning, and cleanup of explanation popups */ export class omdStepVisualizerTextBoxes { constructor(stepVisualizer, highlighting, options = {}) { this.stepVisualizer = stepVisualizer; this.highlighting = highlighting; this.stepTextBoxes = []; this.options = options; } /** * Creates an interactive steps popup for a clicked dot * @param {number} dotIndex - Index of the dot to create text box for */ createTextBoxForDot(dotIndex) { try { this.removeTextBoxForDot(dotIndex); const targetDot = this._findDotAboveForPositioning(dotIndex); if (!targetDot) { console.error('Target dot not found for positioning text box for dot index:', dotIndex); return; } const simplificationData = this._getSimplificationDataForDot(dotIndex); this._createInteractiveStepsForDot(dotIndex, targetDot, simplificationData); } catch (error) { console.error('Error creating text box for dot', dotIndex, ':', error); } } /** * Creates interactive steps for a dot with simplification data * @param {number} dotIndex - Index of the dot * @param {Object} targetDot - The dot to position relative to * @param {Object} simplificationData - Full simplification data including rule names * @private */ _createInteractiveStepsForDot(dotIndex, targetDot, simplificationData) { const interactiveSteps = new omdStepVisualizerInteractiveSteps( this.stepVisualizer, simplificationData, this.options // Pass styling options to interactive steps ); // Position relative to the target dot const x = targetDot.xpos + this.stepVisualizer.dotRadius * 2 + 10; const y = targetDot.ypos - this.stepVisualizer.dotRadius; interactiveSteps.setPosition(x, y); // Set up hover interactions interactiveSteps.setOnStepHover((stepIndex, message, isEntering) => { }); // Set up click interactions interactiveSteps.setOnStepClick((stepIndex, message) => { }); // Add to visual container and track const layoutGroup = interactiveSteps.getLayoutGroup(); layoutGroup.dotIndex = dotIndex; this.stepVisualizer.visualContainer.addChild(layoutGroup); // Apply configured z-index styling to ensure proper layering if (layoutGroup.svgObject && (this.options.zIndex || this.options.position)) { if (this.options.position) { layoutGroup.svgObject.style.position = this.options.position; } if (this.options.zIndex) { layoutGroup.svgObject.style.zIndex = String(this.options.zIndex); // Only apply to the main container, NOT to child elements to preserve internal layout // Applying position absolute to child elements breaks flexbox layout } } this.stepTextBoxes.push({ dotIndex: dotIndex, interactiveSteps: interactiveSteps, layoutGroup: layoutGroup }); // Note: Removed updateVisualLayout call to prevent repositioning movement } /** * Removes the text box for a specific dot * @param {number} dotIndex - Index of the dot to remove text box for */ removeTextBoxForDot(dotIndex) { const textBoxIndex = this.stepTextBoxes.findIndex(tb => tb.dotIndex === dotIndex); if (textBoxIndex >= 0) { const item = this.stepTextBoxes[textBoxIndex]; this.stepVisualizer.visualContainer.removeChild(item.layoutGroup); item.interactiveSteps.destroy(); this.stepTextBoxes.splice(textBoxIndex, 1); // Note: Removed updateVisualLayout call to prevent repositioning movement } } /** * Removes all text boxes */ clearAllTextBoxes() { this.stepTextBoxes.forEach(item => { this.stepVisualizer.visualContainer.removeChild(item.layoutGroup); item.interactiveSteps.destroy(); }); this.stepTextBoxes = []; // Note: Removed updateVisualLayout call to prevent repositioning movement } /** * Finds the appropriate dot above the clicked one for text box positioning * @param {number} dotIndex - Index of the clicked dot * @returns {Object|null} The dot to align with, or null if none found * @private */ _findDotAboveForPositioning(dotIndex) { const currentDot = this.stepVisualizer.stepDots[dotIndex]; if (!currentDot || !currentDot.equationRef) { return null; } const currentEquation = currentDot.equationRef; const currentEquationIndex = this.stepVisualizer.steps.indexOf(currentEquation); // Find the nearest visible equation above for (let i = currentEquationIndex - 1; i >= 0; i--) { const step = this.stepVisualizer.steps[i]; if (step instanceof omdEquationNode && step.visible !== false) { // Find the corresponding dot for (let dotIdx = dotIndex - 1; dotIdx >= 0; dotIdx--) { const dot = this.stepVisualizer.stepDots[dotIdx]; if (dot && dot.equationRef === step) { return dot; } } break; } } // If no visible equation above, use the clicked dot itself return currentDot; } /** * Gets the simplification data for a specific dot * @param {number} dotIndex - Index of the dot * @returns {Object} The simplification data for this step * @private */ _getSimplificationDataForDot(dotIndex) { return this.stepVisualizer._getSimplificationDataForDot(dotIndex); } /** * Gets all text boxes * @returns {Array} Array of text box objects */ getStepTextBoxes() { return this.stepTextBoxes; } /** * Updates the styling options for text boxes * @param {Object} newOptions - New styling options */ updateStyling(newOptions = {}) { this.options = { ...this.options, ...newOptions }; // Apply new styling to existing text boxes this.stepTextBoxes.forEach(textBoxItem => { if (textBoxItem.interactiveSteps && typeof textBoxItem.interactiveSteps.updateStyling === 'function') { textBoxItem.interactiveSteps.updateStyling(this.options); } }); } /** * Gets the current styling options * @returns {Object} Current styling options */ getStyling() { return { ...this.options }; } }