@pie-players/pie-calculator-geogebra 0.3.68

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 ADDED
@@ -0,0 +1,59 @@
1
+ # @pie-players/pie-calculator-geogebra
2
+
3
+ GeoGebra adapter for the provider-neutral `@pie-players/pie-calculator`
4
+ interface. It supports PIE's `scientific` and `graphing` modes and maps `basic`
5
+ to GeoGebra's Scientific Calculator because GeoGebra does not expose a separate
6
+ four-function app through the embedding interface.
7
+
8
+ The package contains PIE-authored adapter code only. By default it loads
9
+ `https://www.geogebra.org/apps/deployggb.js` at runtime; it does not bundle or
10
+ redistribute the GeoGebra application.
11
+
12
+ ## Licensing
13
+
14
+ PIE's adapter is MIT licensed, but GeoGebra is separately licensed. GeoGebra's
15
+ web services and complete application are free for qualifying non-commercial
16
+ use with attribution; commercial use requires a License and Collaboration
17
+ Agreement. Review the current [GeoGebra License](https://www.geogebra.org/license)
18
+ and contact `office@geogebra.org` when the intended use is commercial.
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import { GeoGebraCalculatorProvider } from "@pie-players/pie-calculator-geogebra";
24
+
25
+ const provider = new GeoGebraCalculatorProvider();
26
+ await provider.initialize();
27
+
28
+ const calculator = await provider.createCalculator("graphing", container, {
29
+ restrictedMode: true,
30
+ locale: "en-US",
31
+ settings: {
32
+ showResetIcon: true,
33
+ showZoomButtons: true,
34
+ },
35
+ });
36
+ ```
37
+
38
+ A licensed self-hosted deployment may provide its own loader URL:
39
+
40
+ ```ts
41
+ await provider.initialize({ scriptUrl: "/licensed-geogebra/deployggb.js" });
42
+ ```
43
+
44
+ `initialize()` takes `GeoGebraCalculatorProviderInit` -- `scriptUrl`,
45
+ `appletTimeoutMs`, and `onTelemetry` from the shared `CalculatorProviderInit`.
46
+ The embed takes no credential, so `apiKey` and `proxyEndpoint` are deliberately
47
+ absent. `createCalculator()` takes `GeoGebraCalculatorProviderConfig`, which is
48
+ the provider-neutral configuration with `settings` typed as
49
+ `GeoGebraCalculatorSettings`.
50
+
51
+ The adapter uses GeoGebra's documented `appletOnLoad` API object, Base64 state
52
+ methods, resize methods, and `remove()` cleanup.
53
+
54
+ ## References
55
+
56
+ - [GeoGebra Apps Embedding](https://geogebra.github.io/docs/reference/en/GeoGebra_Apps_Embedding/)
57
+ - [GeoGebra App Parameters](https://geogebra.github.io/docs/reference/en/GeoGebra_App_Parameters/)
58
+ - [GeoGebra Apps API](https://geogebra.github.io/docs/reference/en/GeoGebra_Apps_API/)
59
+ - [GeoGebra License](https://www.geogebra.org/license)
@@ -0,0 +1,94 @@
1
+ import type { Calculator, CalculatorProvider, CalculatorProviderCapabilities, CalculatorProviderConfig, CalculatorProviderInit, CalculatorType } from "@pie-players/pie-calculator";
2
+ interface GeoGebraAppletApi {
3
+ evalCommand?(expression: string): boolean;
4
+ evalCommandCAS?(expression: string): string;
5
+ evalCommandGetLabels?(expression: string): string;
6
+ getBase64?(): string;
7
+ setBase64?(value: string, callback?: () => void): void;
8
+ getEditorState?(): unknown;
9
+ setEditorState?(state: unknown): void;
10
+ newConstruction?(): void;
11
+ setSize?(width: number, height: number): void;
12
+ recalculateEnvironments?(): void;
13
+ remove?(): void;
14
+ }
15
+ interface GeoGebraAppletParameters extends Record<string, unknown> {
16
+ appName: "graphing" | "scientific";
17
+ width: number;
18
+ height: number;
19
+ id: string;
20
+ appletOnLoad: (api: GeoGebraAppletApi) => void;
21
+ }
22
+ interface GeoGebraAppletEmbed {
23
+ inject(target: string): void;
24
+ }
25
+ interface GeoGebraAppletConstructor {
26
+ new (parameters: GeoGebraAppletParameters, useBrowserForJavaScript?: boolean): GeoGebraAppletEmbed;
27
+ }
28
+ declare global {
29
+ interface Window {
30
+ GGBApplet?: GeoGebraAppletConstructor;
31
+ }
32
+ }
33
+ /**
34
+ * GeoGebra's `initialize()` argument.
35
+ *
36
+ * Narrowed to `onTelemetry`: GeoGebra's embed takes no credential, so `apiKey`
37
+ * and `proxyEndpoint` would name a parameter this adapter cannot honour.
38
+ */
39
+ export interface GeoGebraCalculatorProviderInit extends Pick<CalculatorProviderInit, "onTelemetry"> {
40
+ /** Override only when the deployment's GeoGebra license permits that source. */
41
+ scriptUrl?: string;
42
+ /** Maximum time to wait for `appletOnLoad` after injection. */
43
+ appletTimeoutMs?: number;
44
+ }
45
+ /** GeoGebra app parameters accepted through `CalculatorProviderConfig.settings`. */
46
+ export interface GeoGebraCalculatorSettings extends Record<string, unknown> {
47
+ width?: number;
48
+ height?: number;
49
+ borderColor?: string;
50
+ borderRadius?: number;
51
+ enableRightClick?: boolean;
52
+ enableLabelDrags?: boolean;
53
+ enableShiftDragZoom?: boolean;
54
+ showZoomButtons?: boolean;
55
+ errorDialogsActive?: boolean;
56
+ showMenuBar?: boolean;
57
+ showToolBar?: boolean;
58
+ showToolBarHelp?: boolean;
59
+ showAlgebraInput?: boolean;
60
+ showResetIcon?: boolean;
61
+ language?: string;
62
+ country?: string;
63
+ allowStyleBar?: boolean;
64
+ enableFileFeatures?: boolean;
65
+ enableUndoRedo?: boolean;
66
+ enableCAS?: boolean;
67
+ enable3d?: boolean;
68
+ preventFocus?: boolean;
69
+ }
70
+ /**
71
+ * GeoGebra's `createCalculator()` argument: the provider-neutral shape with
72
+ * `settings` naming what it holds.
73
+ */
74
+ export interface GeoGebraCalculatorProviderConfig extends Omit<CalculatorProviderConfig, "settings"> {
75
+ settings?: GeoGebraCalculatorSettings;
76
+ }
77
+ export declare class GeoGebraCalculatorProvider implements CalculatorProvider {
78
+ readonly providerId = "geogebra";
79
+ readonly providerName = "GeoGebra";
80
+ readonly supportedTypes: CalculatorType[];
81
+ readonly version = "6";
82
+ private initialized;
83
+ private appletTimeoutMs;
84
+ private readonly instances;
85
+ private onTelemetry;
86
+ private emitTelemetry;
87
+ private loadGeoGebraScript;
88
+ initialize(config?: GeoGebraCalculatorProviderInit): Promise<void>;
89
+ createCalculator(type: CalculatorType, container: HTMLElement, config?: GeoGebraCalculatorProviderConfig): Promise<Calculator>;
90
+ supportsType(type: CalculatorType): boolean;
91
+ destroy(): void;
92
+ getCapabilities(): CalculatorProviderCapabilities;
93
+ }
94
+ export {};
@@ -0,0 +1,302 @@
1
+ const DEFAULT_SCRIPT_URL = "https://www.geogebra.org/apps/deployggb.js";
2
+ const DEFAULT_APPLET_TIMEOUT_MS = 20_000;
3
+ let nextAppletId = 0;
4
+ export class GeoGebraCalculatorProvider {
5
+ providerId = "geogebra";
6
+ providerName = "GeoGebra";
7
+ supportedTypes = [
8
+ "basic",
9
+ "scientific",
10
+ "graphing",
11
+ ];
12
+ version = "6";
13
+ initialized = false;
14
+ appletTimeoutMs = DEFAULT_APPLET_TIMEOUT_MS;
15
+ /*
16
+ * Every calculator this provider handed out and that has not destroyed itself.
17
+ *
18
+ * `destroy()` is a host's one call to release the provider, and without this it
19
+ * released only the provider's own fields: every applet it created kept running
20
+ * with its container populated and, where the id was generated here, renamed.
21
+ */
22
+ instances = new Set();
23
+ onTelemetry;
24
+ async emitTelemetry(eventName, payload) {
25
+ try {
26
+ await this.onTelemetry?.(eventName, payload);
27
+ }
28
+ catch (error) {
29
+ console.warn("[GeoGebraProvider] telemetry callback failed:", error);
30
+ }
31
+ }
32
+ async loadGeoGebraScript(scriptUrl) {
33
+ await new Promise((resolve, reject) => {
34
+ const script = document.createElement("script");
35
+ script.src = scriptUrl;
36
+ script.async = true;
37
+ script.dataset.pieCalculatorProvider = "geogebra";
38
+ script.onload = () => {
39
+ if (window.GGBApplet)
40
+ resolve();
41
+ else {
42
+ reject(new Error("GeoGebra deploy script loaded but window.GGBApplet is undefined"));
43
+ }
44
+ };
45
+ script.onerror = () => {
46
+ reject(new Error(`Failed to load GeoGebra from ${scriptUrl}`));
47
+ };
48
+ document.head.appendChild(script);
49
+ });
50
+ }
51
+ async initialize(config = {}) {
52
+ if (this.initialized)
53
+ return;
54
+ if (typeof window === "undefined") {
55
+ throw new Error("GeoGebra calculators can only be initialized in the browser");
56
+ }
57
+ this.onTelemetry = config.onTelemetry;
58
+ this.appletTimeoutMs =
59
+ typeof config.appletTimeoutMs === "number" && config.appletTimeoutMs > 0
60
+ ? config.appletTimeoutMs
61
+ : DEFAULT_APPLET_TIMEOUT_MS;
62
+ if (!window.GGBApplet) {
63
+ const scriptUrl = config.scriptUrl?.trim() || DEFAULT_SCRIPT_URL;
64
+ const startedAt = Date.now();
65
+ await this.emitTelemetry("pie-tool-library-load-start", {
66
+ toolId: "calculator",
67
+ backend: "geogebra",
68
+ operation: "geogebra-script-load",
69
+ });
70
+ try {
71
+ await this.loadGeoGebraScript(scriptUrl);
72
+ await this.emitTelemetry("pie-tool-library-load-success", {
73
+ toolId: "calculator",
74
+ backend: "geogebra",
75
+ operation: "geogebra-script-load",
76
+ duration: Date.now() - startedAt,
77
+ });
78
+ }
79
+ catch (error) {
80
+ await this.emitTelemetry("pie-tool-library-load-error", {
81
+ toolId: "calculator",
82
+ backend: "geogebra",
83
+ operation: "geogebra-script-load",
84
+ duration: Date.now() - startedAt,
85
+ errorType: "ToolLibraryLoadError",
86
+ message: error instanceof Error ? error.message : String(error),
87
+ });
88
+ throw error;
89
+ }
90
+ }
91
+ this.initialized = true;
92
+ }
93
+ async createCalculator(type, container, config) {
94
+ if (!this.initialized)
95
+ await this.initialize();
96
+ if (!this.supportsType(type)) {
97
+ throw new Error(`GeoGebra does not support calculator type: ${type}`);
98
+ }
99
+ const calculator = await GeoGebraCalculator.create(this, type, container, config, this.appletTimeoutMs, () => this.instances.delete(calculator));
100
+ this.instances.add(calculator);
101
+ return calculator;
102
+ }
103
+ supportsType(type) {
104
+ return this.supportedTypes.includes(type);
105
+ }
106
+ destroy() {
107
+ // A copy: each `destroy()` calls back to remove itself from the set.
108
+ for (const instance of [...this.instances])
109
+ instance.destroy();
110
+ this.instances.clear();
111
+ this.initialized = false;
112
+ this.appletTimeoutMs = DEFAULT_APPLET_TIMEOUT_MS;
113
+ this.onTelemetry = undefined;
114
+ }
115
+ getCapabilities() {
116
+ return {
117
+ supportsHistory: false,
118
+ supportsGraphing: true,
119
+ supportsExpressions: true,
120
+ canExport: true,
121
+ inputMethods: ["keyboard", "mouse", "touch"],
122
+ };
123
+ }
124
+ }
125
+ class GeoGebraCalculator {
126
+ container;
127
+ onDestroy;
128
+ provider;
129
+ type;
130
+ api = null;
131
+ applet = null;
132
+ generatedContainerId = null;
133
+ destroyed = false;
134
+ constructor(provider, type, container, onDestroy) {
135
+ this.container = container;
136
+ this.onDestroy = onDestroy;
137
+ this.provider = provider;
138
+ this.type = type;
139
+ }
140
+ static async create(provider, type, container, config, timeoutMs, onDestroy) {
141
+ const Constructor = window.GGBApplet;
142
+ if (!Constructor)
143
+ throw new Error("GeoGebra API not available");
144
+ const calculator = new GeoGebraCalculator(provider, type, container, onDestroy);
145
+ const settings = {
146
+ ...(config?.settings || {}),
147
+ };
148
+ delete settings.appName;
149
+ delete settings.id;
150
+ delete settings.appletOnLoad;
151
+ const width = typeof settings.width === "number"
152
+ ? settings.width
153
+ : Math.max(container.clientWidth || 0, 320);
154
+ const height = typeof settings.height === "number"
155
+ ? settings.height
156
+ : Math.max(container.clientHeight || 0, 320);
157
+ delete settings.width;
158
+ delete settings.height;
159
+ const isRestricted = config?.restrictedMode === true;
160
+ const appName = type === "graphing" ? "graphing" : "scientific";
161
+ const appletId = `pie-geogebra-${Date.now()}-${++nextAppletId}`;
162
+ if (!container.id) {
163
+ calculator.generatedContainerId = `${appletId}-container`;
164
+ container.id = calculator.generatedContainerId;
165
+ }
166
+ const clearGeneratedContainerId = () => {
167
+ if (calculator.generatedContainerId &&
168
+ container.id === calculator.generatedContainerId) {
169
+ container.id = "";
170
+ }
171
+ calculator.generatedContainerId = null;
172
+ };
173
+ await new Promise((resolve, reject) => {
174
+ let settled = false;
175
+ const timer = setTimeout(() => {
176
+ settled = true;
177
+ container.replaceChildren();
178
+ clearGeneratedContainerId();
179
+ reject(new Error(`GeoGebra applet did not initialize within ${timeoutMs}ms`));
180
+ }, timeoutMs);
181
+ const parameters = {
182
+ showMenuBar: false,
183
+ showToolBar: false,
184
+ // GeoGebra's scientific embed otherwise renders an empty algebra view:
185
+ // the input row is the calculator's primary interaction surface.
186
+ showAlgebraInput: true,
187
+ showZoomButtons: appName === "graphing",
188
+ ...settings,
189
+ enable3d: false,
190
+ preventFocus: true,
191
+ ...(isRestricted
192
+ ? {
193
+ showMenuBar: false,
194
+ showToolBar: false,
195
+ enableFileFeatures: false,
196
+ enableCAS: false,
197
+ enableRightClick: false,
198
+ }
199
+ : {}),
200
+ appName,
201
+ width,
202
+ height,
203
+ id: appletId,
204
+ appletOnLoad: (api) => {
205
+ clearTimeout(timer);
206
+ if (settled) {
207
+ api.remove?.();
208
+ return;
209
+ }
210
+ settled = true;
211
+ calculator.api = api;
212
+ resolve();
213
+ },
214
+ };
215
+ try {
216
+ container.replaceChildren();
217
+ calculator.applet = new Constructor(parameters, true);
218
+ calculator.applet.inject(container.id);
219
+ }
220
+ catch (error) {
221
+ clearTimeout(timer);
222
+ settled = true;
223
+ container.replaceChildren();
224
+ clearGeneratedContainerId();
225
+ reject(error);
226
+ }
227
+ });
228
+ return calculator;
229
+ }
230
+ getValue() {
231
+ const editorState = this.api?.getEditorState?.();
232
+ return editorState === undefined ? "" : JSON.stringify(editorState);
233
+ }
234
+ setValue(value) {
235
+ if (!value || !this.api?.setEditorState)
236
+ return;
237
+ try {
238
+ this.api.setEditorState(JSON.parse(value));
239
+ }
240
+ catch (error) {
241
+ console.warn("[GeoGebraCalculator] Failed to restore editor state:", error);
242
+ }
243
+ }
244
+ clear() {
245
+ this.api?.newConstruction?.();
246
+ }
247
+ async evaluate(expression) {
248
+ const casResult = this.api?.evalCommandCAS?.(expression);
249
+ if (typeof casResult === "string" && casResult.length > 0)
250
+ return casResult;
251
+ const labels = this.api?.evalCommandGetLabels?.(expression);
252
+ if (typeof labels === "string" && labels.length > 0)
253
+ return labels;
254
+ return this.api?.evalCommand?.(expression) ? expression : "";
255
+ }
256
+ resize() {
257
+ if (!this.api)
258
+ return;
259
+ const width = Math.max(this.container.clientWidth || 0, 1);
260
+ const height = Math.max(this.container.clientHeight || 0, 1);
261
+ this.api.setSize?.(width, height);
262
+ this.api.recalculateEnvironments?.();
263
+ }
264
+ focus() {
265
+ const target = this.container.querySelector('input, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])');
266
+ target?.focus();
267
+ }
268
+ exportState() {
269
+ return {
270
+ type: this.type,
271
+ provider: "geogebra",
272
+ value: this.getValue(),
273
+ providerState: this.api?.getBase64?.() || "",
274
+ };
275
+ }
276
+ importState(state) {
277
+ if (state.provider !== "geogebra") {
278
+ throw new Error(`Cannot import state from provider: ${state.provider}`);
279
+ }
280
+ if (typeof state.providerState === "string" && state.providerState) {
281
+ this.api?.setBase64?.(state.providerState);
282
+ }
283
+ else if (state.value) {
284
+ this.setValue(state.value);
285
+ }
286
+ }
287
+ destroy() {
288
+ if (this.destroyed)
289
+ return;
290
+ this.destroyed = true;
291
+ this.api?.remove?.();
292
+ this.api = null;
293
+ this.applet = null;
294
+ this.container.replaceChildren();
295
+ if (this.generatedContainerId &&
296
+ this.container.id === this.generatedContainerId) {
297
+ this.container.id = "";
298
+ }
299
+ this.generatedContainerId = null;
300
+ this.onDestroy();
301
+ }
302
+ }
@@ -0,0 +1,3 @@
1
+ /** GeoGebra adapter for the provider-neutral PIE calculator seam. */
2
+ export { GeoGebraCalculatorProvider } from "./geogebra-provider.js";
3
+ export type { GeoGebraCalculatorProviderConfig, GeoGebraCalculatorProviderInit, GeoGebraCalculatorSettings, } from "./geogebra-provider.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /** GeoGebra adapter for the provider-neutral PIE calculator seam. */
2
+ export { GeoGebraCalculatorProvider } from "./geogebra-provider.js";
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@pie-players/pie-calculator-geogebra",
3
+ "version": "0.3.68",
4
+ "description": "GeoGebra calculator provider adapter for PIE assessment tools",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "bun test tests",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "keywords": [
24
+ "pie",
25
+ "calculator",
26
+ "geogebra",
27
+ "graphing",
28
+ "scientific",
29
+ "math",
30
+ "accessibility"
31
+ ],
32
+ "author": "PIE Framework",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/pie-framework/pie-players.git",
37
+ "directory": "packages/calculator-geogebra"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@pie-players/pie-calculator": "0.3.68"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "sideEffects": false,
49
+ "homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/calculator-geogebra#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/pie-framework/pie-players/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=20.0.0"
55
+ }
56
+ }