@teachinglab/omd 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -57
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
|
@@ -1,502 +1,502 @@
|
|
|
1
|
-
import { omdEquationSequenceNode } from '../nodes/omdEquationSequenceNode.js';
|
|
2
|
-
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
3
|
-
import { omdColor } from '../../src/omdColor.js';
|
|
4
|
-
import { jsvgGroup, jsvgRect, jsvgLayoutGroup, jsvgButton } from '@teachinglab/jsvg';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A toolbar component for applying mathematical operations to an omdEquationSequenceNode.
|
|
8
|
-
*/
|
|
9
|
-
export class omdToolbar {
|
|
10
|
-
/**
|
|
11
|
-
* Creates an instance of the omdToolbar.
|
|
12
|
-
* @param {jsvgGroup} parentContainer - The parent SVG group to render the toolbar into.
|
|
13
|
-
* @param {omdEquationSequenceNode} sequence - The sequence node to apply operations to.
|
|
14
|
-
* @param {object} [options={}] - Configuration options for the toolbar.
|
|
15
|
-
*/
|
|
16
|
-
constructor(parentContainer, sequence, options = {}) {
|
|
17
|
-
this.parentContainer = parentContainer;
|
|
18
|
-
this.sequence = sequence;
|
|
19
|
-
|
|
20
|
-
this.config = {
|
|
21
|
-
height: 60, padding: 6, spacing: 8, borderRadius: 30,
|
|
22
|
-
fontFamily: "'Albert Sans', sans-serif", fontWeight: '500',
|
|
23
|
-
colors: { background: omdColor.mediumGray, button: 'white', popup: omdColor.lightGray, undo: '#87D143' },
|
|
24
|
-
buttonSize: 48, checkMarkSize: 24, mainFontSize: 32,
|
|
25
|
-
inputFontSize: 28, menuFontSize: 24, inputWidth: 120,
|
|
26
|
-
popupDirection: 'below',
|
|
27
|
-
showUndoButton: false,
|
|
28
|
-
undoIconUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzciIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCAzNyA0NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xOC4zNjc2IDQzLjgzMDFDMTMuNTkxMyA0My44MjM3IDkuMDEyNTYgNDEuOTIzNCA1LjYzNTI1IDM4LjU0NTlDMi4yNTc5MiAzNS4xNjg1IDAuMzU3NjYgMzAuNTg5OSAwLjM1MTA3NCAyNS44MTM2QzAuMzUxMDc0IDI1LjMxOTQgMC41NDc0NDEgMjQuODQ1MiAwLjg5Njk2MSAyNC40OTU4QzEuMjQ2NDggMjQuMTQ2MiAxLjcyMDU1IDIzLjk0OTggMi4yMTQ4NSAyMy45NDk4QzIuNzA5MTUgMjMuOTQ5OCAzLjE4MzIyIDI0LjE0NjIgMy41MzI3NCAyNC40OTU4QzMuODgyMjYgMjQuODQ1MiA0LjA3ODYzIDI1LjMxOTQgNC4wNzg2MyAyNS44MTM2QzQuMDc4NjMgMjguNjM5NiA0LjkxNjY2IDMxLjQwMjIgNi40ODY3NSAzMy43NTIxQzguMDU2ODUgMzYuMTAxOSAxMC4yODg1IDM3LjkzMzQgMTIuODk5NCAzOS4wMTQ5QzE1LjUxMDMgNDAuMDk2NCAxOC4zODM1IDQwLjM3OTQgMjEuMTU1MyAzOS44MjhDMjMuOTI3MSAzOS4yNzY4IDI2LjQ3MyAzNy45MTU3IDI4LjQ3MTUgMzUuOTE3NUMzMC40Njk3IDMzLjkxOTEgMzEuODMwOCAzMS4zNzMxIDMyLjM4MTkgMjguNjAxM0MzMi45MzM0IDI1LjgyOTUgMzIuNjUwMyAyMi45NTYzIDMxLjU2ODggMjAuMzQ1NkMzMC40ODczIDE3LjczNDUgMjguNjU1OSAxNS41MDI5IDI2LjMwNiAxMy45MzI4QzIzLjk1NjIgMTIuMzYyNyAyMS4xOTM2IDExLjUyNDcgMTguMzY3NiAxMS41MjQ3SDEyLjE1NUMxMS42NjA3IDExLjUyNDcgMTEuMTg2NiAxMS4zMjgzIDEwLjgzNzEgMTAuOTc4OEMxMC40ODc2IDEwLjYyOTMgMTAuMjkxMiAxMC4xNTUyIDEwLjI5MTIgOS42NjA5QzEwLjI5MTIgOS4xNjY2IDEwLjQ4NzYgOC42OTI1MyAxMC44MzcxIDguMzQzMDFDMTEuMTg2NiA3Ljk5MzQ5IDExLjY2MDcgNy43OTcxMiAxMi4xNTUgNy43OTcxMkgxOC4zNjc2QzIzLjE0NTggNy43OTcxMiAyNy43Mjg1IDkuNjk1MjkgMzEuMTA3MSAxMy4wNzRDMzQuNDg2IDE2LjQ1MjggMzYuMzg0MSAyMS4wMzU0IDM2LjM4NDEgMjUuODEzNkMzNi4zODQxIDMwLjU5MTkgMzQuNDg2IDM1LjE3NDUgMzEuMTA3MSAzOC41NTMyQzI3LjcyODUgNDEuOTMyMSAyMy4xNDU4IDQzLjgzMDEgMTguMzY3NiA0My44MzAxWiIgZmlsbD0id2hpdGUiLz4NCjxwYXRoIGQ9Ik0xOC4zNjc1IDE4Ljk3OThDMTguMTIyNyAxOC45ODEgMTcuODc5OSAxOC45MzMzIDE3LjY1MzggMTguODM5NEMxNy40Mjc3IDE4Ljc0NTQgMTcuMjIyNyAxOC42MDczIDE3LjA1MDQgMTguNDMzMUw5LjU5NTM2IDEwLjk3OEM5LjI0NjM0IDEwLjYyODYgOS4wNTAyOSAxMC4xNTQ5IDkuMDUwMjkgOS42NjA5NkM5LjA1MDI5IDkuMTY3MDYgOS4yNDYzNCA4LjY5MzM2IDkuNTk1MzYgOC4zNDM4OUwxNy4wNTA0IDAuODg4Nzg5QzE3LjIyMTIgMC43MDU2NjcgMTcuNDI2OSAwLjU1ODgwMSAxNy42NTU1IDAuNDU2OTM5QzE3Ljg4NDIgMC4zNTUwNzggMTguMTMwOSAwLjMwMDMwOCAxOC4zODEyIDAuMjk1ODg0QzE4LjYzMTQgMC4yOTE0NjEgMTguODc5OSAwLjMzNzUwOCAxOS4xMTIgMC40MzEyNDRDMTkuMzQ0MSAwLjUyNDk3OSAxOS41NTQ5IDAuNjY0NDg5IDE5LjczMTggMC44NDE0NzNDMTkuOTA5IDEuMDE4NDYgMjAuMDQ4NCAxLjIyOTI5IDIwLjE0MjEgMS40NjEzNEMyMC4yMzYgMS42OTM0MiAyMC4yODIgMS45NDIgMjAuMjc3NSAyLjE5MjI0QzIwLjI3MyAyLjQ0MjUxIDIwLjIxODQgMi42ODkzIDIwLjExNjUgMi45MTc5MkMyMC4wMTQ2IDMuMTQ2NTQgMTkuODY3NyAzLjM1MjMgMTkuNjg0NiAzLjUyMjkzTDEzLjU0NjUgOS42NjA5NkwxOS42ODQ2IDE1Ljc5OUMyMC4wMzM3IDE2LjE0ODUgMjAuMjI5OCAxNi42MjIyIDIwLjIyOTggMTcuMTE2QzIwLjIyOTggMTcuNjEgMjAuMDMzNyAxOC4wODM3IDE5LjY4NDYgMTguNDMzMUMxOS41MTI2IDE4LjYwNzMgMTkuMzA3MyAxOC43NDU0IDE5LjA4MTIgMTguODM5NEMxOC44NTUxIDE4LjkzMzMgMTguNjEyNSAxOC45ODEgMTguMzY3NSAxOC45Nzk4WiIgZmlsbD0id2hpdGUiLz4NCjwvc3ZnPg0K',
|
|
29
|
-
onUndo: null,
|
|
30
|
-
...options
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Support structured styles from equation stack toolbar options
|
|
34
|
-
if (options.styles && typeof options.styles === 'object') {
|
|
35
|
-
const s = options.styles;
|
|
36
|
-
if (s.backgroundColor) this.config.colors.background = s.backgroundColor;
|
|
37
|
-
if (s.buttonColor) this.config.colors.button = s.buttonColor;
|
|
38
|
-
if (s.popupBackgroundColor) this.config.colors.popup = s.popupBackgroundColor;
|
|
39
|
-
if (typeof s.borderRadius === 'number') this.config.borderRadius = s.borderRadius;
|
|
40
|
-
if (typeof s.buttonSize === 'number') this.config.buttonSize = s.buttonSize;
|
|
41
|
-
if (typeof s.mainFontSize === 'number') this.config.mainFontSize = s.mainFontSize;
|
|
42
|
-
if (typeof s.inputFontSize === 'number') this.config.inputFontSize = s.inputFontSize;
|
|
43
|
-
if (typeof s.menuFontSize === 'number') this.config.menuFontSize = s.menuFontSize;
|
|
44
|
-
if (typeof s.inputWidth === 'number') this.config.inputWidth = s.inputWidth;
|
|
45
|
-
if (typeof s.padding === 'number') this.config.padding = s.padding;
|
|
46
|
-
if (typeof s.spacing === 'number') this.config.spacing = s.spacing;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Simple aliases remain supported
|
|
50
|
-
if (options.backgroundColor) this.config.colors.background = options.backgroundColor;
|
|
51
|
-
if (options.popupBackgroundColor) this.config.colors.popup = options.popupBackgroundColor;
|
|
52
|
-
|
|
53
|
-
// If no explicit popup color was provided, default it to the toolbar background color
|
|
54
|
-
const popupProvided = !!(options.popupBackgroundColor || (options.styles && options.styles.popupBackgroundColor));
|
|
55
|
-
if (!popupProvided) {
|
|
56
|
-
this.config.colors.popup = this.config.colors.background;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.state = {
|
|
60
|
-
activePopup: null,
|
|
61
|
-
selectedOperation: '+',
|
|
62
|
-
inputValue: ''
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
this.elements = {};
|
|
66
|
-
this._render();
|
|
67
|
-
this._updateApplyButtonState();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Renders the initial toolbar UI components.
|
|
72
|
-
* @private
|
|
73
|
-
*/
|
|
74
|
-
_render() {
|
|
75
|
-
this.elements.toolbarGroup = new jsvgGroup();
|
|
76
|
-
this.parentContainer.addChild(this.elements.toolbarGroup);
|
|
77
|
-
if (this.config.x || this.config.y) {
|
|
78
|
-
this.elements.toolbarGroup.setPosition(this.config.x || 0, this.config.y || 0);
|
|
79
|
-
}
|
|
80
|
-
this.elements.toolbarGroup.svgObject.style.userSelect = 'none';
|
|
81
|
-
|
|
82
|
-
this.elements.background = new jsvgRect();
|
|
83
|
-
this.elements.background.setWidthAndHeight(362, this.config.height);
|
|
84
|
-
this.elements.background.setCornerRadius(this.config.borderRadius);
|
|
85
|
-
this.elements.background.setFillColor(this.config.colors.background);
|
|
86
|
-
this.elements.toolbarGroup.addChild(this.elements.background);
|
|
87
|
-
|
|
88
|
-
this.elements.leftButton = this._createButton({
|
|
89
|
-
text: this.state.selectedOperation,
|
|
90
|
-
callback: () => this._togglePopup('operations')
|
|
91
|
-
});
|
|
92
|
-
this.elements.toolbarGroup.addChild(this.elements.leftButton);
|
|
93
|
-
|
|
94
|
-
this.elements.middleInputButton = this._createButton({
|
|
95
|
-
width: this.config.inputWidth,
|
|
96
|
-
text: this.state.inputValue,
|
|
97
|
-
fontSize: this.config.inputFontSize,
|
|
98
|
-
cornerRadius: 10,
|
|
99
|
-
callback: () => this._togglePopup('input')
|
|
100
|
-
});
|
|
101
|
-
this.elements.toolbarGroup.addChild(this.elements.middleInputButton);
|
|
102
|
-
|
|
103
|
-
const checkmarkSVG = `<svg width="43" height="33" viewBox="0 0 43 33" xmlns="http://www.w3.org/2000/svg"><rect x="9.86" y="28.63" width="40.04" height="5.74" transform="rotate(-45 9.86 28.63)" fill="black"/><rect x="13.9" y="32.69" width="19.64" height="5.74" transform="rotate(-135 13.9 32.69)" fill="black"/></svg>`;
|
|
104
|
-
this.elements.rightButton = this._createButton({
|
|
105
|
-
svg: checkmarkSVG,
|
|
106
|
-
callback: () => this._applyOperation()
|
|
107
|
-
});
|
|
108
|
-
this.elements.toolbarGroup.addChild(this.elements.rightButton);
|
|
109
|
-
|
|
110
|
-
if (this.config.showUndoButton) {
|
|
111
|
-
this.elements.undoButton = this._createButton({
|
|
112
|
-
size: this.config.buttonSize,
|
|
113
|
-
iconUrl: this.config.undoIconUrl,
|
|
114
|
-
callback: () => this._handleUndo()
|
|
115
|
-
});
|
|
116
|
-
// Set the circular fill color to requested green
|
|
117
|
-
this.elements.undoButton.setFillColor(this.config.colors.undo || '#87D143');
|
|
118
|
-
this.elements.toolbarGroup.addChild(this.elements.undoButton);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this._updateToolbarLayout();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Moves an SVG element to the top of its parent's stacking order.
|
|
126
|
-
* @param {jsvgObject|undefined} node
|
|
127
|
-
* @private
|
|
128
|
-
*/
|
|
129
|
-
_bringToFront(node) {
|
|
130
|
-
try {
|
|
131
|
-
const el = node?.svgObject;
|
|
132
|
-
const parent = el?.parentNode;
|
|
133
|
-
if (el && parent) {
|
|
134
|
-
parent.appendChild(el);
|
|
135
|
-
}
|
|
136
|
-
} catch (_) { /* no-op */ }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Toggles the visibility of a popup menu (operations or input).
|
|
141
|
-
* Ensures only one popup is visible at a time.
|
|
142
|
-
* @param {string} popupType - The type of popup to toggle ('operations' or 'input').
|
|
143
|
-
* @private
|
|
144
|
-
*/
|
|
145
|
-
_togglePopup(popupType) {
|
|
146
|
-
if (this.state.activePopup && this.state.activePopup.type === popupType) {
|
|
147
|
-
// Remove from toolbar group to keep transform context consistent
|
|
148
|
-
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
149
|
-
this.state.activePopup = null;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (this.state.activePopup) {
|
|
154
|
-
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
155
|
-
this.state.activePopup = null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
let popupGroup;
|
|
159
|
-
if (popupType === 'operations') {
|
|
160
|
-
popupGroup = this._renderOperationsMenu();
|
|
161
|
-
} else if (popupType === 'input') {
|
|
162
|
-
popupGroup = this.state.selectedOperation === 'f'
|
|
163
|
-
? this._renderFunctionMenu()
|
|
164
|
-
: this._renderDigitGrid();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (popupGroup) {
|
|
168
|
-
// Attach to toolbar group so it inherits toolbar counter-scaling (keeps constant on-screen size)
|
|
169
|
-
this.elements.toolbarGroup.addChild(popupGroup);
|
|
170
|
-
this.state.activePopup = { type: popupType, group: popupGroup };
|
|
171
|
-
// Ensure the toolbar and popup are on top of all siblings inside the SVG
|
|
172
|
-
this._bringToFront(this.elements.toolbarGroup);
|
|
173
|
-
this._bringToFront(popupGroup);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Creates and positions a generic popup group.
|
|
179
|
-
* @param {Function} contentFactory - A function that returns the jsvgGroup content for the popup.
|
|
180
|
-
* @param {jsvgGroup} anchorButton - The button to anchor the popup to.
|
|
181
|
-
* @returns {jsvgGroup} The fully rendered and positioned popup group.
|
|
182
|
-
* @private
|
|
183
|
-
*/
|
|
184
|
-
_renderPopup(contentFactory, anchorButton) {
|
|
185
|
-
const popupGroup = new jsvgGroup();
|
|
186
|
-
// Ensure popup captures interactions and overlays content
|
|
187
|
-
if (popupGroup.svgObject) {
|
|
188
|
-
popupGroup.svgObject.style.pointerEvents = 'auto';
|
|
189
|
-
}
|
|
190
|
-
const content = contentFactory();
|
|
191
|
-
|
|
192
|
-
const bgWidth = content.width + 16;
|
|
193
|
-
const bgHeight = content.height + 16;
|
|
194
|
-
|
|
195
|
-
const background = new jsvgRect();
|
|
196
|
-
background.setWidthAndHeight(bgWidth, bgHeight);
|
|
197
|
-
background.setCornerRadius(this.config.borderRadius);
|
|
198
|
-
background.setFillColor(this.config.colors.popup);
|
|
199
|
-
popupGroup.addChild(background);
|
|
200
|
-
|
|
201
|
-
content.setPosition(8, 8);
|
|
202
|
-
popupGroup.addChild(content);
|
|
203
|
-
popupGroup.width = bgWidth;
|
|
204
|
-
popupGroup.height = bgHeight;
|
|
205
|
-
|
|
206
|
-
// Anchor centered horizontally on the button, in toolbar-local coordinates
|
|
207
|
-
const popupX = anchorButton.xpos + (anchorButton.width / 2) - (bgWidth / 2);
|
|
208
|
-
|
|
209
|
-
// Determine vertical placement based on configuration (toolbar-local coordinates)
|
|
210
|
-
const placeAbove = String(this.config.popupDirection || 'below') === 'above';
|
|
211
|
-
const popupY = placeAbove
|
|
212
|
-
? (anchorButton.ypos) - bgHeight - this.config.spacing
|
|
213
|
-
: (anchorButton.ypos + anchorButton.height + this.config.spacing);
|
|
214
|
-
popupGroup.setPosition(Math.round(popupX), Math.round(popupY));
|
|
215
|
-
|
|
216
|
-
return popupGroup;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Renders the operations menu popup ('f', '÷', '×', '–', '+').
|
|
221
|
-
* @returns {jsvgGroup} The rendered operations menu group.
|
|
222
|
-
* @private
|
|
223
|
-
*/
|
|
224
|
-
_renderOperationsMenu() {
|
|
225
|
-
const operations = ['f', '÷', '×', '–', '+'];
|
|
226
|
-
return this._renderPopup(() => {
|
|
227
|
-
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
228
|
-
operations.forEach(op => {
|
|
229
|
-
const button = this._createButton({ text: op, fontSize: this.config.menuFontSize, callback: () => this._selectOperation(op) });
|
|
230
|
-
layout.addChild(button);
|
|
231
|
-
});
|
|
232
|
-
layout.doVerticalLayout();
|
|
233
|
-
return layout;
|
|
234
|
-
}, this.elements.leftButton);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Renders the function selection menu popup ('sqrt', 'cos', etc.).
|
|
239
|
-
* @returns {jsvgGroup} The rendered function menu group.
|
|
240
|
-
* @private
|
|
241
|
-
*/
|
|
242
|
-
_renderFunctionMenu() {
|
|
243
|
-
const functions = ['sqrt', 'cos', 'sin', 'tan', 'ln'];
|
|
244
|
-
return this._renderPopup(() => {
|
|
245
|
-
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
246
|
-
functions.forEach(func => {
|
|
247
|
-
const button = this._createButton({
|
|
248
|
-
width: 80, height: 48, cornerRadius: 10, text: func,
|
|
249
|
-
fontSize: this.config.inputFontSize, callback: () => this._handleFunctionClick(func)
|
|
250
|
-
});
|
|
251
|
-
layout.addChild(button);
|
|
252
|
-
});
|
|
253
|
-
layout.doVerticalLayout();
|
|
254
|
-
return layout;
|
|
255
|
-
}, this.elements.middleInputButton);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Renders the digit grid (number pad) popup.
|
|
260
|
-
* @returns {jsvgGroup} The rendered digit grid group.
|
|
261
|
-
* @private
|
|
262
|
-
*/
|
|
263
|
-
_renderDigitGrid() {
|
|
264
|
-
const digits = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['←', '0', 'x'] ];
|
|
265
|
-
return this._renderPopup(() => {
|
|
266
|
-
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
267
|
-
digits.forEach(rowItems => {
|
|
268
|
-
const rowGroup = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
269
|
-
rowItems.forEach(digit => {
|
|
270
|
-
const button = this._createButton({ text: digit, fontSize: this.config.inputFontSize, callback: () => this._handleDigitClick(digit) });
|
|
271
|
-
rowGroup.addChild(button);
|
|
272
|
-
});
|
|
273
|
-
rowGroup.doHorizontalLayout();
|
|
274
|
-
layout.addChild(rowGroup);
|
|
275
|
-
});
|
|
276
|
-
layout.doVerticalLayout();
|
|
277
|
-
return layout;
|
|
278
|
-
}, this.elements.middleInputButton);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Handles clicks on the function menu buttons.
|
|
283
|
-
* @param {string} func - The name of the function that was clicked.
|
|
284
|
-
* @private
|
|
285
|
-
*/
|
|
286
|
-
_handleFunctionClick(func) {
|
|
287
|
-
this.setInputText(func);
|
|
288
|
-
this._togglePopup('input');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Handles clicks on the digit grid buttons.
|
|
293
|
-
* @param {string} digit - The digit or action ('←') that was clicked.
|
|
294
|
-
* @private
|
|
295
|
-
*/
|
|
296
|
-
_handleDigitClick(digit) {
|
|
297
|
-
if (digit === '←') {
|
|
298
|
-
this.state.inputValue = this.state.inputValue.slice(0, -1);
|
|
299
|
-
} else {
|
|
300
|
-
this.state.inputValue += digit;
|
|
301
|
-
}
|
|
302
|
-
this.setInputText(this.state.inputValue);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Sets the text of the middle input button.
|
|
307
|
-
* @param {string} text - The text to display.
|
|
308
|
-
*/
|
|
309
|
-
setInputText(text) {
|
|
310
|
-
this.state.inputValue = text;
|
|
311
|
-
const button = this.elements.middleInputButton;
|
|
312
|
-
button.setText(text);
|
|
313
|
-
|
|
314
|
-
// Get the button's text element and set font size
|
|
315
|
-
const textElement = button.buttonText;
|
|
316
|
-
textElement.setFontSize(this.config.inputFontSize);
|
|
317
|
-
|
|
318
|
-
this._updateApplyButtonState();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Handles the selection of a new operation from the menu.
|
|
323
|
-
* @param {string} operation - The selected operation symbol.
|
|
324
|
-
* @private
|
|
325
|
-
*/
|
|
326
|
-
_selectOperation(operation) {
|
|
327
|
-
// Clear input text when switching to or from function mode.
|
|
328
|
-
if (this.state.selectedOperation === 'f' || operation === 'f') {
|
|
329
|
-
this.setInputText('');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
this.state.selectedOperation = operation;
|
|
333
|
-
this.elements.leftButton.setText(operation);
|
|
334
|
-
this._togglePopup('operations');
|
|
335
|
-
|
|
336
|
-
// If we switched to function mode and the input popup was open, refresh it.
|
|
337
|
-
if (operation === 'f' && this.state.activePopup?.type === 'input') {
|
|
338
|
-
this._togglePopup('input'); // Close number pad
|
|
339
|
-
this._togglePopup('input'); // Open function menu
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Applies the selected operation and value to the sequence.
|
|
345
|
-
* @private
|
|
346
|
-
*/
|
|
347
|
-
_applyOperation() {
|
|
348
|
-
const op = this.state.selectedOperation;
|
|
349
|
-
const val = this.state.inputValue;
|
|
350
|
-
|
|
351
|
-
if (!this.sequence || val === '') return;
|
|
352
|
-
|
|
353
|
-
if (op === 'f') {
|
|
354
|
-
this.sequence.applyEquationFunction(val);
|
|
355
|
-
} else {
|
|
356
|
-
const operationMap = {
|
|
357
|
-
'÷': 'divide',
|
|
358
|
-
'×': 'multiply',
|
|
359
|
-
'–': 'subtract',
|
|
360
|
-
'+': 'add'
|
|
361
|
-
};
|
|
362
|
-
const operationName = operationMap[op];
|
|
363
|
-
let valueToApply;
|
|
364
|
-
let isValid = false;
|
|
365
|
-
// Try to parse as number first
|
|
366
|
-
const numericValue = parseFloat(val);
|
|
367
|
-
if (!isNaN(numericValue) && String(numericValue) === val.trim()) {
|
|
368
|
-
valueToApply = numericValue;
|
|
369
|
-
isValid = true;
|
|
370
|
-
} else if (typeof window.math !== 'undefined' && window.math.parse) {
|
|
371
|
-
try {
|
|
372
|
-
valueToApply = window.math.parse(val);
|
|
373
|
-
isValid = true;
|
|
374
|
-
} catch (e) {
|
|
375
|
-
isValid = false;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (operationName && isValid) {
|
|
379
|
-
this.sequence.applyEquationOperation(valueToApply, operationName);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Clear the input after applying the operation
|
|
384
|
-
this.setInputText('');
|
|
385
|
-
|
|
386
|
-
if (this.state.activePopup) {
|
|
387
|
-
// Remove from toolbar group where it was attached
|
|
388
|
-
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
389
|
-
this.state.activePopup = null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Notify host to refresh display and any active external visualizations
|
|
393
|
-
try {
|
|
394
|
-
if (typeof window !== 'undefined') {
|
|
395
|
-
if (typeof window.refreshDisplayAndFilters === 'function') {
|
|
396
|
-
window.refreshDisplayAndFilters();
|
|
397
|
-
}
|
|
398
|
-
if (typeof window.onOMDOperationApplied === 'function') {
|
|
399
|
-
window.onOMDOperationApplied(this.sequence);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} catch (_) { /* no-op */ }
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Creates a button component with the specified configuration.
|
|
407
|
-
* @param {object} config - The button configuration.
|
|
408
|
-
* @param {number} [config.width] - The width of the button.
|
|
409
|
-
* @param {number} [config.height] - The height of the button.
|
|
410
|
-
* @param {number} [config.size] - The size for both width and height.
|
|
411
|
-
* @param {string} [config.text] - The text label for the button.
|
|
412
|
-
* @param {string} [config.svg] - The SVG content for the button icon.
|
|
413
|
-
* @param {number} [config.fontSize] - The font size for the text.
|
|
414
|
-
* @param {number} [config.cornerRadius] - The corner radius of the button.
|
|
415
|
-
* @param {Function} config.callback - The function to call on click.
|
|
416
|
-
* @returns {jsvgButton} The created button.
|
|
417
|
-
* @private
|
|
418
|
-
*/
|
|
419
|
-
_createButton({ width, height, size, text, svg, iconUrl, fontSize, cornerRadius, callback }) {
|
|
420
|
-
const button = new jsvgButton();
|
|
421
|
-
const w = width || size || this.config.buttonSize;
|
|
422
|
-
const h = height || size || this.config.buttonSize;
|
|
423
|
-
button.setWidthAndHeight(w, h);
|
|
424
|
-
button.setCornerRadius(cornerRadius !== undefined ? cornerRadius : w / 2);
|
|
425
|
-
button.setFillColor(this.config.colors.button);
|
|
426
|
-
button.setText(text || '');
|
|
427
|
-
button.setFontSize(fontSize || this.config.mainFontSize);
|
|
428
|
-
button.setFontFamily(this.config.fontFamily);
|
|
429
|
-
button.buttonText.setFontWeight(this.config.fontWeight);
|
|
430
|
-
|
|
431
|
-
// Adjust vertical position of text for better centering
|
|
432
|
-
button.buttonText.setPosition(w/2, h/2 + (fontSize || this.config.mainFontSize)/3);
|
|
433
|
-
|
|
434
|
-
if (svg) {
|
|
435
|
-
const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
436
|
-
button.addImage(dataURI, this.config.checkMarkSize, this.config.checkMarkSize);
|
|
437
|
-
} else if (iconUrl) {
|
|
438
|
-
// Use default icon size (same as checkmark) centered inside the circular button
|
|
439
|
-
const sz = this.config.checkMarkSize;
|
|
440
|
-
button.addImage(iconUrl, sz, sz);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
button.setClickCallback(callback);
|
|
444
|
-
return button;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Updates the enabled/disabled state of the apply button.
|
|
449
|
-
* @private
|
|
450
|
-
*/
|
|
451
|
-
_updateApplyButtonState() {
|
|
452
|
-
const button = this.elements.rightButton;
|
|
453
|
-
const hasValue = this.state.inputValue.length > 0;
|
|
454
|
-
|
|
455
|
-
if (hasValue) {
|
|
456
|
-
button.setOpacity(1.0);
|
|
457
|
-
button.setClickCallback(() => this._applyOperation());
|
|
458
|
-
} else {
|
|
459
|
-
button.setOpacity(0.5);
|
|
460
|
-
button.setClickCallback(null);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Updates the positions of the toolbar elements.
|
|
466
|
-
* @private
|
|
467
|
-
*/
|
|
468
|
-
_updateToolbarLayout() {
|
|
469
|
-
const totalWidth = this.config.buttonSize * 2 + this.config.inputWidth + this.config.spacing * 2 + this.config.padding * 2;
|
|
470
|
-
this.elements.background.setWidth(totalWidth);
|
|
471
|
-
|
|
472
|
-
const yPos = this.config.padding;
|
|
473
|
-
let xPos = this.config.padding;
|
|
474
|
-
|
|
475
|
-
this.elements.leftButton.setPosition(xPos, yPos);
|
|
476
|
-
xPos += this.elements.leftButton.width + this.config.spacing;
|
|
477
|
-
|
|
478
|
-
this.elements.middleInputButton.setPosition(xPos, yPos);
|
|
479
|
-
xPos += this.elements.middleInputButton.width + this.config.spacing;
|
|
480
|
-
|
|
481
|
-
this.elements.rightButton.setPosition(xPos, yPos);
|
|
482
|
-
|
|
483
|
-
// Position optional undo button directly to the right of the toolbar background
|
|
484
|
-
if (this.elements.undoButton) {
|
|
485
|
-
const undoX = this.elements.background.width + this.config.spacing;
|
|
486
|
-
this.elements.undoButton.setPosition(undoX, yPos);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
_handleUndo() {
|
|
491
|
-
if (typeof this.config.onUndo === 'function') {
|
|
492
|
-
try { this.config.onUndo(this.sequence); } catch (_) {}
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
// Fallback: emit a global hook
|
|
496
|
-
try {
|
|
497
|
-
if (typeof window !== 'undefined' && typeof window.onOMDToolbarUndo === 'function') {
|
|
498
|
-
window.onOMDToolbarUndo(this.sequence);
|
|
499
|
-
}
|
|
500
|
-
} catch (_) { /* no-op */ }
|
|
501
|
-
}
|
|
1
|
+
import { omdEquationSequenceNode } from '../nodes/omdEquationSequenceNode.js';
|
|
2
|
+
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
3
|
+
import { omdColor } from '../../src/omdColor.js';
|
|
4
|
+
import { jsvgGroup, jsvgRect, jsvgLayoutGroup, jsvgButton } from '@teachinglab/jsvg';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A toolbar component for applying mathematical operations to an omdEquationSequenceNode.
|
|
8
|
+
*/
|
|
9
|
+
export class omdToolbar {
|
|
10
|
+
/**
|
|
11
|
+
* Creates an instance of the omdToolbar.
|
|
12
|
+
* @param {jsvgGroup} parentContainer - The parent SVG group to render the toolbar into.
|
|
13
|
+
* @param {omdEquationSequenceNode} sequence - The sequence node to apply operations to.
|
|
14
|
+
* @param {object} [options={}] - Configuration options for the toolbar.
|
|
15
|
+
*/
|
|
16
|
+
constructor(parentContainer, sequence, options = {}) {
|
|
17
|
+
this.parentContainer = parentContainer;
|
|
18
|
+
this.sequence = sequence;
|
|
19
|
+
|
|
20
|
+
this.config = {
|
|
21
|
+
height: 60, padding: 6, spacing: 8, borderRadius: 30,
|
|
22
|
+
fontFamily: "'Albert Sans', sans-serif", fontWeight: '500',
|
|
23
|
+
colors: { background: omdColor.mediumGray, button: 'white', popup: omdColor.lightGray, undo: '#87D143' },
|
|
24
|
+
buttonSize: 48, checkMarkSize: 24, mainFontSize: 32,
|
|
25
|
+
inputFontSize: 28, menuFontSize: 24, inputWidth: 120,
|
|
26
|
+
popupDirection: 'below',
|
|
27
|
+
showUndoButton: false,
|
|
28
|
+
undoIconUrl: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzciIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCAzNyA0NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xOC4zNjc2IDQzLjgzMDFDMTMuNTkxMyA0My44MjM3IDkuMDEyNTYgNDEuOTIzNCA1LjYzNTI1IDM4LjU0NTlDMi4yNTc5MiAzNS4xNjg1IDAuMzU3NjYgMzAuNTg5OSAwLjM1MTA3NCAyNS44MTM2QzAuMzUxMDc0IDI1LjMxOTQgMC41NDc0NDEgMjQuODQ1MiAwLjg5Njk2MSAyNC40OTU4QzEuMjQ2NDggMjQuMTQ2MiAxLjcyMDU1IDIzLjk0OTggMi4yMTQ4NSAyMy45NDk4QzIuNzA5MTUgMjMuOTQ5OCAzLjE4MzIyIDI0LjE0NjIgMy41MzI3NCAyNC40OTU4QzMuODgyMjYgMjQuODQ1MiA0LjA3ODYzIDI1LjMxOTQgNC4wNzg2MyAyNS44MTM2QzQuMDc4NjMgMjguNjM5NiA0LjkxNjY2IDMxLjQwMjIgNi40ODY3NSAzMy43NTIxQzguMDU2ODUgMzYuMTAxOSAxMC4yODg1IDM3LjkzMzQgMTIuODk5NCAzOS4wMTQ5QzE1LjUxMDMgNDAuMDk2NCAxOC4zODM1IDQwLjM3OTQgMjEuMTU1MyAzOS44MjhDMjMuOTI3MSAzOS4yNzY4IDI2LjQ3MyAzNy45MTU3IDI4LjQ3MTUgMzUuOTE3NUMzMC40Njk3IDMzLjkxOTEgMzEuODMwOCAzMS4zNzMxIDMyLjM4MTkgMjguNjAxM0MzMi45MzM0IDI1LjgyOTUgMzIuNjUwMyAyMi45NTYzIDMxLjU2ODggMjAuMzQ1NkMzMC40ODczIDE3LjczNDUgMjguNjU1OSAxNS41MDI5IDI2LjMwNiAxMy45MzI4QzIzLjk1NjIgMTIuMzYyNyAyMS4xOTM2IDExLjUyNDcgMTguMzY3NiAxMS41MjQ3SDEyLjE1NUMxMS42NjA3IDExLjUyNDcgMTEuMTg2NiAxMS4zMjgzIDEwLjgzNzEgMTAuOTc4OEMxMC40ODc2IDEwLjYyOTMgMTAuMjkxMiAxMC4xNTUyIDEwLjI5MTIgOS42NjA5QzEwLjI5MTIgOS4xNjY2IDEwLjQ4NzYgOC42OTI1MyAxMC44MzcxIDguMzQzMDFDMTEuMTg2NiA3Ljk5MzQ5IDExLjY2MDcgNy43OTcxMiAxMi4xNTUgNy43OTcxMkgxOC4zNjc2QzIzLjE0NTggNy43OTcxMiAyNy43Mjg1IDkuNjk1MjkgMzEuMTA3MSAxMy4wNzRDMzQuNDg2IDE2LjQ1MjggMzYuMzg0MSAyMS4wMzU0IDM2LjM4NDEgMjUuODEzNkMzNi4zODQxIDMwLjU5MTkgMzQuNDg2IDM1LjE3NDUgMzEuMTA3MSAzOC41NTMyQzI3LjcyODUgNDEuOTMyMSAyMy4xNDU4IDQzLjgzMDEgMTguMzY3NiA0My44MzAxWiIgZmlsbD0id2hpdGUiLz4NCjxwYXRoIGQ9Ik0xOC4zNjc1IDE4Ljk3OThDMTguMTIyNyAxOC45ODEgMTcuODc5OSAxOC45MzMzIDE3LjY1MzggMTguODM5NEMxNy40Mjc3IDE4Ljc0NTQgMTcuMjIyNyAxOC42MDczIDE3LjA1MDQgMTguNDMzMUw5LjU5NTM2IDEwLjk3OEM5LjI0NjM0IDEwLjYyODYgOS4wNTAyOSAxMC4xNTQ5IDkuMDUwMjkgOS42NjA5NkM5LjA1MDI5IDkuMTY3MDYgOS4yNDYzNCA4LjY5MzM2IDkuNTk1MzYgOC4zNDM4OUwxNy4wNTA0IDAuODg4Nzg5QzE3LjIyMTIgMC43MDU2NjcgMTcuNDI2OSAwLjU1ODgwMSAxNy42NTU1IDAuNDU2OTM5QzE3Ljg4NDIgMC4zNTUwNzggMTguMTMwOSAwLjMwMDMwOCAxOC4zODEyIDAuMjk1ODg0QzE4LjYzMTQgMC4yOTE0NjEgMTguODc5OSAwLjMzNzUwOCAxOS4xMTIgMC40MzEyNDRDMTkuMzQ0MSAwLjUyNDk3OSAxOS41NTQ5IDAuNjY0NDg5IDE5LjczMTggMC44NDE0NzNDMTkuOTA5IDEuMDE4NDYgMjAuMDQ4NCAxLjIyOTI5IDIwLjE0MjEgMS40NjEzNEMyMC4yMzYgMS42OTM0MiAyMC4yODIgMS45NDIgMjAuMjc3NSAyLjE5MjI0QzIwLjI3MyAyLjQ0MjUxIDIwLjIxODQgMi42ODkzIDIwLjExNjUgMi45MTc5MkMyMC4wMTQ2IDMuMTQ2NTQgMTkuODY3NyAzLjM1MjMgMTkuNjg0NiAzLjUyMjkzTDEzLjU0NjUgOS42NjA5NkwxOS42ODQ2IDE1Ljc5OUMyMC4wMzM3IDE2LjE0ODUgMjAuMjI5OCAxNi42MjIyIDIwLjIyOTggMTcuMTE2QzIwLjIyOTggMTcuNjEgMjAuMDMzNyAxOC4wODM3IDE5LjY4NDYgMTguNDMzMUMxOS41MTI2IDE4LjYwNzMgMTkuMzA3MyAxOC43NDU0IDE5LjA4MTIgMTguODM5NEMxOC44NTUxIDE4LjkzMzMgMTguNjEyNSAxOC45ODEgMTguMzY3NSAxOC45Nzk4WiIgZmlsbD0id2hpdGUiLz4NCjwvc3ZnPg0K',
|
|
29
|
+
onUndo: null,
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Support structured styles from equation stack toolbar options
|
|
34
|
+
if (options.styles && typeof options.styles === 'object') {
|
|
35
|
+
const s = options.styles;
|
|
36
|
+
if (s.backgroundColor) this.config.colors.background = s.backgroundColor;
|
|
37
|
+
if (s.buttonColor) this.config.colors.button = s.buttonColor;
|
|
38
|
+
if (s.popupBackgroundColor) this.config.colors.popup = s.popupBackgroundColor;
|
|
39
|
+
if (typeof s.borderRadius === 'number') this.config.borderRadius = s.borderRadius;
|
|
40
|
+
if (typeof s.buttonSize === 'number') this.config.buttonSize = s.buttonSize;
|
|
41
|
+
if (typeof s.mainFontSize === 'number') this.config.mainFontSize = s.mainFontSize;
|
|
42
|
+
if (typeof s.inputFontSize === 'number') this.config.inputFontSize = s.inputFontSize;
|
|
43
|
+
if (typeof s.menuFontSize === 'number') this.config.menuFontSize = s.menuFontSize;
|
|
44
|
+
if (typeof s.inputWidth === 'number') this.config.inputWidth = s.inputWidth;
|
|
45
|
+
if (typeof s.padding === 'number') this.config.padding = s.padding;
|
|
46
|
+
if (typeof s.spacing === 'number') this.config.spacing = s.spacing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Simple aliases remain supported
|
|
50
|
+
if (options.backgroundColor) this.config.colors.background = options.backgroundColor;
|
|
51
|
+
if (options.popupBackgroundColor) this.config.colors.popup = options.popupBackgroundColor;
|
|
52
|
+
|
|
53
|
+
// If no explicit popup color was provided, default it to the toolbar background color
|
|
54
|
+
const popupProvided = !!(options.popupBackgroundColor || (options.styles && options.styles.popupBackgroundColor));
|
|
55
|
+
if (!popupProvided) {
|
|
56
|
+
this.config.colors.popup = this.config.colors.background;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.state = {
|
|
60
|
+
activePopup: null,
|
|
61
|
+
selectedOperation: '+',
|
|
62
|
+
inputValue: ''
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.elements = {};
|
|
66
|
+
this._render();
|
|
67
|
+
this._updateApplyButtonState();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders the initial toolbar UI components.
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_render() {
|
|
75
|
+
this.elements.toolbarGroup = new jsvgGroup();
|
|
76
|
+
this.parentContainer.addChild(this.elements.toolbarGroup);
|
|
77
|
+
if (this.config.x || this.config.y) {
|
|
78
|
+
this.elements.toolbarGroup.setPosition(this.config.x || 0, this.config.y || 0);
|
|
79
|
+
}
|
|
80
|
+
this.elements.toolbarGroup.svgObject.style.userSelect = 'none';
|
|
81
|
+
|
|
82
|
+
this.elements.background = new jsvgRect();
|
|
83
|
+
this.elements.background.setWidthAndHeight(362, this.config.height);
|
|
84
|
+
this.elements.background.setCornerRadius(this.config.borderRadius);
|
|
85
|
+
this.elements.background.setFillColor(this.config.colors.background);
|
|
86
|
+
this.elements.toolbarGroup.addChild(this.elements.background);
|
|
87
|
+
|
|
88
|
+
this.elements.leftButton = this._createButton({
|
|
89
|
+
text: this.state.selectedOperation,
|
|
90
|
+
callback: () => this._togglePopup('operations')
|
|
91
|
+
});
|
|
92
|
+
this.elements.toolbarGroup.addChild(this.elements.leftButton);
|
|
93
|
+
|
|
94
|
+
this.elements.middleInputButton = this._createButton({
|
|
95
|
+
width: this.config.inputWidth,
|
|
96
|
+
text: this.state.inputValue,
|
|
97
|
+
fontSize: this.config.inputFontSize,
|
|
98
|
+
cornerRadius: 10,
|
|
99
|
+
callback: () => this._togglePopup('input')
|
|
100
|
+
});
|
|
101
|
+
this.elements.toolbarGroup.addChild(this.elements.middleInputButton);
|
|
102
|
+
|
|
103
|
+
const checkmarkSVG = `<svg width="43" height="33" viewBox="0 0 43 33" xmlns="http://www.w3.org/2000/svg"><rect x="9.86" y="28.63" width="40.04" height="5.74" transform="rotate(-45 9.86 28.63)" fill="black"/><rect x="13.9" y="32.69" width="19.64" height="5.74" transform="rotate(-135 13.9 32.69)" fill="black"/></svg>`;
|
|
104
|
+
this.elements.rightButton = this._createButton({
|
|
105
|
+
svg: checkmarkSVG,
|
|
106
|
+
callback: () => this._applyOperation()
|
|
107
|
+
});
|
|
108
|
+
this.elements.toolbarGroup.addChild(this.elements.rightButton);
|
|
109
|
+
|
|
110
|
+
if (this.config.showUndoButton) {
|
|
111
|
+
this.elements.undoButton = this._createButton({
|
|
112
|
+
size: this.config.buttonSize,
|
|
113
|
+
iconUrl: this.config.undoIconUrl,
|
|
114
|
+
callback: () => this._handleUndo()
|
|
115
|
+
});
|
|
116
|
+
// Set the circular fill color to requested green
|
|
117
|
+
this.elements.undoButton.setFillColor(this.config.colors.undo || '#87D143');
|
|
118
|
+
this.elements.toolbarGroup.addChild(this.elements.undoButton);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this._updateToolbarLayout();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Moves an SVG element to the top of its parent's stacking order.
|
|
126
|
+
* @param {jsvgObject|undefined} node
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
_bringToFront(node) {
|
|
130
|
+
try {
|
|
131
|
+
const el = node?.svgObject;
|
|
132
|
+
const parent = el?.parentNode;
|
|
133
|
+
if (el && parent) {
|
|
134
|
+
parent.appendChild(el);
|
|
135
|
+
}
|
|
136
|
+
} catch (_) { /* no-op */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Toggles the visibility of a popup menu (operations or input).
|
|
141
|
+
* Ensures only one popup is visible at a time.
|
|
142
|
+
* @param {string} popupType - The type of popup to toggle ('operations' or 'input').
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
_togglePopup(popupType) {
|
|
146
|
+
if (this.state.activePopup && this.state.activePopup.type === popupType) {
|
|
147
|
+
// Remove from toolbar group to keep transform context consistent
|
|
148
|
+
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
149
|
+
this.state.activePopup = null;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (this.state.activePopup) {
|
|
154
|
+
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
155
|
+
this.state.activePopup = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let popupGroup;
|
|
159
|
+
if (popupType === 'operations') {
|
|
160
|
+
popupGroup = this._renderOperationsMenu();
|
|
161
|
+
} else if (popupType === 'input') {
|
|
162
|
+
popupGroup = this.state.selectedOperation === 'f'
|
|
163
|
+
? this._renderFunctionMenu()
|
|
164
|
+
: this._renderDigitGrid();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (popupGroup) {
|
|
168
|
+
// Attach to toolbar group so it inherits toolbar counter-scaling (keeps constant on-screen size)
|
|
169
|
+
this.elements.toolbarGroup.addChild(popupGroup);
|
|
170
|
+
this.state.activePopup = { type: popupType, group: popupGroup };
|
|
171
|
+
// Ensure the toolbar and popup are on top of all siblings inside the SVG
|
|
172
|
+
this._bringToFront(this.elements.toolbarGroup);
|
|
173
|
+
this._bringToFront(popupGroup);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates and positions a generic popup group.
|
|
179
|
+
* @param {Function} contentFactory - A function that returns the jsvgGroup content for the popup.
|
|
180
|
+
* @param {jsvgGroup} anchorButton - The button to anchor the popup to.
|
|
181
|
+
* @returns {jsvgGroup} The fully rendered and positioned popup group.
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_renderPopup(contentFactory, anchorButton) {
|
|
185
|
+
const popupGroup = new jsvgGroup();
|
|
186
|
+
// Ensure popup captures interactions and overlays content
|
|
187
|
+
if (popupGroup.svgObject) {
|
|
188
|
+
popupGroup.svgObject.style.pointerEvents = 'auto';
|
|
189
|
+
}
|
|
190
|
+
const content = contentFactory();
|
|
191
|
+
|
|
192
|
+
const bgWidth = content.width + 16;
|
|
193
|
+
const bgHeight = content.height + 16;
|
|
194
|
+
|
|
195
|
+
const background = new jsvgRect();
|
|
196
|
+
background.setWidthAndHeight(bgWidth, bgHeight);
|
|
197
|
+
background.setCornerRadius(this.config.borderRadius);
|
|
198
|
+
background.setFillColor(this.config.colors.popup);
|
|
199
|
+
popupGroup.addChild(background);
|
|
200
|
+
|
|
201
|
+
content.setPosition(8, 8);
|
|
202
|
+
popupGroup.addChild(content);
|
|
203
|
+
popupGroup.width = bgWidth;
|
|
204
|
+
popupGroup.height = bgHeight;
|
|
205
|
+
|
|
206
|
+
// Anchor centered horizontally on the button, in toolbar-local coordinates
|
|
207
|
+
const popupX = anchorButton.xpos + (anchorButton.width / 2) - (bgWidth / 2);
|
|
208
|
+
|
|
209
|
+
// Determine vertical placement based on configuration (toolbar-local coordinates)
|
|
210
|
+
const placeAbove = String(this.config.popupDirection || 'below') === 'above';
|
|
211
|
+
const popupY = placeAbove
|
|
212
|
+
? (anchorButton.ypos) - bgHeight - this.config.spacing
|
|
213
|
+
: (anchorButton.ypos + anchorButton.height + this.config.spacing);
|
|
214
|
+
popupGroup.setPosition(Math.round(popupX), Math.round(popupY));
|
|
215
|
+
|
|
216
|
+
return popupGroup;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Renders the operations menu popup ('f', '÷', '×', '–', '+').
|
|
221
|
+
* @returns {jsvgGroup} The rendered operations menu group.
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
_renderOperationsMenu() {
|
|
225
|
+
const operations = ['f', '÷', '×', '–', '+'];
|
|
226
|
+
return this._renderPopup(() => {
|
|
227
|
+
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
228
|
+
operations.forEach(op => {
|
|
229
|
+
const button = this._createButton({ text: op, fontSize: this.config.menuFontSize, callback: () => this._selectOperation(op) });
|
|
230
|
+
layout.addChild(button);
|
|
231
|
+
});
|
|
232
|
+
layout.doVerticalLayout();
|
|
233
|
+
return layout;
|
|
234
|
+
}, this.elements.leftButton);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Renders the function selection menu popup ('sqrt', 'cos', etc.).
|
|
239
|
+
* @returns {jsvgGroup} The rendered function menu group.
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
_renderFunctionMenu() {
|
|
243
|
+
const functions = ['sqrt', 'cos', 'sin', 'tan', 'ln'];
|
|
244
|
+
return this._renderPopup(() => {
|
|
245
|
+
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
246
|
+
functions.forEach(func => {
|
|
247
|
+
const button = this._createButton({
|
|
248
|
+
width: 80, height: 48, cornerRadius: 10, text: func,
|
|
249
|
+
fontSize: this.config.inputFontSize, callback: () => this._handleFunctionClick(func)
|
|
250
|
+
});
|
|
251
|
+
layout.addChild(button);
|
|
252
|
+
});
|
|
253
|
+
layout.doVerticalLayout();
|
|
254
|
+
return layout;
|
|
255
|
+
}, this.elements.middleInputButton);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Renders the digit grid (number pad) popup.
|
|
260
|
+
* @returns {jsvgGroup} The rendered digit grid group.
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
_renderDigitGrid() {
|
|
264
|
+
const digits = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['←', '0', 'x'] ];
|
|
265
|
+
return this._renderPopup(() => {
|
|
266
|
+
const layout = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
267
|
+
digits.forEach(rowItems => {
|
|
268
|
+
const rowGroup = new jsvgLayoutGroup({ spacer: this.config.spacing });
|
|
269
|
+
rowItems.forEach(digit => {
|
|
270
|
+
const button = this._createButton({ text: digit, fontSize: this.config.inputFontSize, callback: () => this._handleDigitClick(digit) });
|
|
271
|
+
rowGroup.addChild(button);
|
|
272
|
+
});
|
|
273
|
+
rowGroup.doHorizontalLayout();
|
|
274
|
+
layout.addChild(rowGroup);
|
|
275
|
+
});
|
|
276
|
+
layout.doVerticalLayout();
|
|
277
|
+
return layout;
|
|
278
|
+
}, this.elements.middleInputButton);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Handles clicks on the function menu buttons.
|
|
283
|
+
* @param {string} func - The name of the function that was clicked.
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
286
|
+
_handleFunctionClick(func) {
|
|
287
|
+
this.setInputText(func);
|
|
288
|
+
this._togglePopup('input');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handles clicks on the digit grid buttons.
|
|
293
|
+
* @param {string} digit - The digit or action ('←') that was clicked.
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_handleDigitClick(digit) {
|
|
297
|
+
if (digit === '←') {
|
|
298
|
+
this.state.inputValue = this.state.inputValue.slice(0, -1);
|
|
299
|
+
} else {
|
|
300
|
+
this.state.inputValue += digit;
|
|
301
|
+
}
|
|
302
|
+
this.setInputText(this.state.inputValue);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Sets the text of the middle input button.
|
|
307
|
+
* @param {string} text - The text to display.
|
|
308
|
+
*/
|
|
309
|
+
setInputText(text) {
|
|
310
|
+
this.state.inputValue = text;
|
|
311
|
+
const button = this.elements.middleInputButton;
|
|
312
|
+
button.setText(text);
|
|
313
|
+
|
|
314
|
+
// Get the button's text element and set font size
|
|
315
|
+
const textElement = button.buttonText;
|
|
316
|
+
textElement.setFontSize(this.config.inputFontSize);
|
|
317
|
+
|
|
318
|
+
this._updateApplyButtonState();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handles the selection of a new operation from the menu.
|
|
323
|
+
* @param {string} operation - The selected operation symbol.
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_selectOperation(operation) {
|
|
327
|
+
// Clear input text when switching to or from function mode.
|
|
328
|
+
if (this.state.selectedOperation === 'f' || operation === 'f') {
|
|
329
|
+
this.setInputText('');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.state.selectedOperation = operation;
|
|
333
|
+
this.elements.leftButton.setText(operation);
|
|
334
|
+
this._togglePopup('operations');
|
|
335
|
+
|
|
336
|
+
// If we switched to function mode and the input popup was open, refresh it.
|
|
337
|
+
if (operation === 'f' && this.state.activePopup?.type === 'input') {
|
|
338
|
+
this._togglePopup('input'); // Close number pad
|
|
339
|
+
this._togglePopup('input'); // Open function menu
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Applies the selected operation and value to the sequence.
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
_applyOperation() {
|
|
348
|
+
const op = this.state.selectedOperation;
|
|
349
|
+
const val = this.state.inputValue;
|
|
350
|
+
|
|
351
|
+
if (!this.sequence || val === '') return;
|
|
352
|
+
|
|
353
|
+
if (op === 'f') {
|
|
354
|
+
this.sequence.applyEquationFunction(val);
|
|
355
|
+
} else {
|
|
356
|
+
const operationMap = {
|
|
357
|
+
'÷': 'divide',
|
|
358
|
+
'×': 'multiply',
|
|
359
|
+
'–': 'subtract',
|
|
360
|
+
'+': 'add'
|
|
361
|
+
};
|
|
362
|
+
const operationName = operationMap[op];
|
|
363
|
+
let valueToApply;
|
|
364
|
+
let isValid = false;
|
|
365
|
+
// Try to parse as number first
|
|
366
|
+
const numericValue = parseFloat(val);
|
|
367
|
+
if (!isNaN(numericValue) && String(numericValue) === val.trim()) {
|
|
368
|
+
valueToApply = numericValue;
|
|
369
|
+
isValid = true;
|
|
370
|
+
} else if (typeof window.math !== 'undefined' && window.math.parse) {
|
|
371
|
+
try {
|
|
372
|
+
valueToApply = window.math.parse(val);
|
|
373
|
+
isValid = true;
|
|
374
|
+
} catch (e) {
|
|
375
|
+
isValid = false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (operationName && isValid) {
|
|
379
|
+
this.sequence.applyEquationOperation(valueToApply, operationName);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Clear the input after applying the operation
|
|
384
|
+
this.setInputText('');
|
|
385
|
+
|
|
386
|
+
if (this.state.activePopup) {
|
|
387
|
+
// Remove from toolbar group where it was attached
|
|
388
|
+
this.elements.toolbarGroup.removeChild(this.state.activePopup.group);
|
|
389
|
+
this.state.activePopup = null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Notify host to refresh display and any active external visualizations
|
|
393
|
+
try {
|
|
394
|
+
if (typeof window !== 'undefined') {
|
|
395
|
+
if (typeof window.refreshDisplayAndFilters === 'function') {
|
|
396
|
+
window.refreshDisplayAndFilters();
|
|
397
|
+
}
|
|
398
|
+
if (typeof window.onOMDOperationApplied === 'function') {
|
|
399
|
+
window.onOMDOperationApplied(this.sequence);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (_) { /* no-op */ }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Creates a button component with the specified configuration.
|
|
407
|
+
* @param {object} config - The button configuration.
|
|
408
|
+
* @param {number} [config.width] - The width of the button.
|
|
409
|
+
* @param {number} [config.height] - The height of the button.
|
|
410
|
+
* @param {number} [config.size] - The size for both width and height.
|
|
411
|
+
* @param {string} [config.text] - The text label for the button.
|
|
412
|
+
* @param {string} [config.svg] - The SVG content for the button icon.
|
|
413
|
+
* @param {number} [config.fontSize] - The font size for the text.
|
|
414
|
+
* @param {number} [config.cornerRadius] - The corner radius of the button.
|
|
415
|
+
* @param {Function} config.callback - The function to call on click.
|
|
416
|
+
* @returns {jsvgButton} The created button.
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
_createButton({ width, height, size, text, svg, iconUrl, fontSize, cornerRadius, callback }) {
|
|
420
|
+
const button = new jsvgButton();
|
|
421
|
+
const w = width || size || this.config.buttonSize;
|
|
422
|
+
const h = height || size || this.config.buttonSize;
|
|
423
|
+
button.setWidthAndHeight(w, h);
|
|
424
|
+
button.setCornerRadius(cornerRadius !== undefined ? cornerRadius : w / 2);
|
|
425
|
+
button.setFillColor(this.config.colors.button);
|
|
426
|
+
button.setText(text || '');
|
|
427
|
+
button.setFontSize(fontSize || this.config.mainFontSize);
|
|
428
|
+
button.setFontFamily(this.config.fontFamily);
|
|
429
|
+
button.buttonText.setFontWeight(this.config.fontWeight);
|
|
430
|
+
|
|
431
|
+
// Adjust vertical position of text for better centering
|
|
432
|
+
button.buttonText.setPosition(w/2, h/2 + (fontSize || this.config.mainFontSize)/3);
|
|
433
|
+
|
|
434
|
+
if (svg) {
|
|
435
|
+
const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
436
|
+
button.addImage(dataURI, this.config.checkMarkSize, this.config.checkMarkSize);
|
|
437
|
+
} else if (iconUrl) {
|
|
438
|
+
// Use default icon size (same as checkmark) centered inside the circular button
|
|
439
|
+
const sz = this.config.checkMarkSize;
|
|
440
|
+
button.addImage(iconUrl, sz, sz);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
button.setClickCallback(callback);
|
|
444
|
+
return button;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Updates the enabled/disabled state of the apply button.
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
_updateApplyButtonState() {
|
|
452
|
+
const button = this.elements.rightButton;
|
|
453
|
+
const hasValue = this.state.inputValue.length > 0;
|
|
454
|
+
|
|
455
|
+
if (hasValue) {
|
|
456
|
+
button.setOpacity(1.0);
|
|
457
|
+
button.setClickCallback(() => this._applyOperation());
|
|
458
|
+
} else {
|
|
459
|
+
button.setOpacity(0.5);
|
|
460
|
+
button.setClickCallback(null);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Updates the positions of the toolbar elements.
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
_updateToolbarLayout() {
|
|
469
|
+
const totalWidth = this.config.buttonSize * 2 + this.config.inputWidth + this.config.spacing * 2 + this.config.padding * 2;
|
|
470
|
+
this.elements.background.setWidth(totalWidth);
|
|
471
|
+
|
|
472
|
+
const yPos = this.config.padding;
|
|
473
|
+
let xPos = this.config.padding;
|
|
474
|
+
|
|
475
|
+
this.elements.leftButton.setPosition(xPos, yPos);
|
|
476
|
+
xPos += this.elements.leftButton.width + this.config.spacing;
|
|
477
|
+
|
|
478
|
+
this.elements.middleInputButton.setPosition(xPos, yPos);
|
|
479
|
+
xPos += this.elements.middleInputButton.width + this.config.spacing;
|
|
480
|
+
|
|
481
|
+
this.elements.rightButton.setPosition(xPos, yPos);
|
|
482
|
+
|
|
483
|
+
// Position optional undo button directly to the right of the toolbar background
|
|
484
|
+
if (this.elements.undoButton) {
|
|
485
|
+
const undoX = this.elements.background.width + this.config.spacing;
|
|
486
|
+
this.elements.undoButton.setPosition(undoX, yPos);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_handleUndo() {
|
|
491
|
+
if (typeof this.config.onUndo === 'function') {
|
|
492
|
+
try { this.config.onUndo(this.sequence); } catch (_) {}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Fallback: emit a global hook
|
|
496
|
+
try {
|
|
497
|
+
if (typeof window !== 'undefined' && typeof window.onOMDToolbarUndo === 'function') {
|
|
498
|
+
window.onOMDToolbarUndo(this.sequence);
|
|
499
|
+
}
|
|
500
|
+
} catch (_) { /* no-op */ }
|
|
501
|
+
}
|
|
502
502
|
}
|