From 71294b680e71d88cd1fb7b87b52835eaabb522df Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 21 Dec 2025 19:55:48 -0600 Subject: [PATCH] Upgrade JSXGraph to the latest version and fix the graphtool for that. The latest version of JXSGraph is 1.12.2. However, that version (and all versions after 1.11.1) have an issue with tab order when keyboard events are enabled. Basically, it is impossible to use shift-tab to progress in reverse in the tab order. See https://github.com/jsxgraph/jsxgraph/issues/773. So to work around that I had to override the board's `keyDownListener` method with one that does not call `preventDefault` on a keydown event that comes from a tab key being used. Another thing that is a bit annoying with versions 1.11.1 and later is that you now have to set the tabindex on elements that are not fixed yourself. By default they set the tabindex to -1, which means they are not in the tab order. So `gt.definingPointAttributes` is now a function, and if it is called with `gt.isStatic` true, a tabindex of -1 is used, but if `gt.isStatic` is false, then a tabindex of 0 is used (and so those points will be keyboard focusable). --- htdocs/js/GraphTool/circletool.js | 2 +- htdocs/js/GraphTool/cubictool.js | 2 +- htdocs/js/GraphTool/graphtool.js | 116 ++++++++++++++++++++++++++- htdocs/js/GraphTool/intervaltools.js | 3 +- htdocs/js/GraphTool/linetool.js | 2 +- htdocs/js/GraphTool/parabolatool.js | 2 +- htdocs/js/GraphTool/pointtool.js | 3 +- htdocs/js/GraphTool/quadratictool.js | 2 +- htdocs/js/GraphTool/quadrilateral.js | 2 +- htdocs/js/GraphTool/sinewavetool.js | 2 +- htdocs/js/GraphTool/triangle.js | 2 +- htdocs/package-lock.json | 14 ++-- htdocs/package.json | 2 +- 13 files changed, 132 insertions(+), 22 deletions(-) diff --git a/htdocs/js/GraphTool/circletool.js b/htdocs/js/GraphTool/circletool.js index 50b9c663fc..af51d27acf 100644 --- a/htdocs/js/GraphTool/circletool.js +++ b/htdocs/js/GraphTool/circletool.js @@ -188,7 +188,7 @@ const center = this.center; delete this.center; - center.setAttribute(gt.definingPointAttributes); + center.setAttribute(gt.definingPointAttributes()); center.on('down', () => gt.onPointDown(center)); center.on('up', () => gt.onPointUp(center)); diff --git a/htdocs/js/GraphTool/cubictool.js b/htdocs/js/GraphTool/cubictool.js index c65983a3f4..46b4e23b27 100644 --- a/htdocs/js/GraphTool/cubictool.js +++ b/htdocs/js/GraphTool/cubictool.js @@ -12,7 +12,7 @@ constructor(point1, point2, point3, point4, solid) { for (const point of [point1, point2, point3, point4]) { - point.setAttribute(gt.definingPointAttributes); + point.setAttribute(gt.definingPointAttributes()); if (!gt.isStatic) { point.on('down', () => gt.onPointDown(point)); point.on('up', () => gt.onPointUp(point)); diff --git a/htdocs/js/GraphTool/graphtool.js b/htdocs/js/GraphTool/graphtool.js index c674837746..65373a1729 100644 --- a/htdocs/js/GraphTool/graphtool.js +++ b/htdocs/js/GraphTool/graphtool.js @@ -39,7 +39,7 @@ window.graphTool = (containerId, options) => { underConstructionFixed: JXG.palette.red // defined to be '#d55e00' }; - gt.definingPointAttributes = { + gt.definingPointAttributes = () => ({ size: 3, fixed: false, highlight: true, @@ -49,8 +49,9 @@ window.graphTool = (containerId, options) => { fillColor: gt.color.point, highlightStrokeWidth: 1, highlightStrokeColor: gt.color.focusCurve, - highlightFillColor: gt.color.pointHighlight - }; + highlightFillColor: gt.color.pointHighlight, + tabindex: gt.isStatic ? -1 : 0 + }); gt.options = options; gt.snapSizeX = options.snapSizeX ? options.snapSizeX : 1; @@ -262,6 +263,113 @@ window.graphTool = (containerId, options) => { gt.board.highlightInfobox = (_x, _y, el) => gt.board.highlightCustomInfobox('', el); if (!gt.isStatic) { + // This is a mess to work around an issue with JSXGraph versions 1.11.1 or later. Their keyDownListener + // calls preventDefault on the keydown event when shift-tab is pressed. That prevents keyboard focus from + // moving backward in the tab order. So this removes the JSXGraph keyboard event handlers, then overrides + // the board's keyDownListener with essentially the same code with the exception that when the tab key is + // pressed, preventDefault is not called. Then the keyboard event listeners are added back, using this + // keydownListener. + gt.board.removeKeyboardEventHandlers(); + gt.board.keyDownListener = function (evt) { + const id_node = evt.target.id; + let done = true; + + if (!this.attr.keyboard.enabled || id_node === '') return false; + + const doc = this.containerObj.shadowRoot || document; + if (doc.activeElement) { + if (doc.activeElement.tagName === 'INPUT' || doc.activeElement.tagName === 'textarea') return false; + } + + const id = id_node.replace(this.containerObj.id + '_', ''); + const el = this.select(id); + + if ( + (JXG.evaluate(this.attr.keyboard.panshift) && evt.shiftKey) || + (JXG.evaluate(this.attr.keyboard.panctrl) && evt.ctrlKey) + ) { + const doZoom = JXG.evaluate(this.attr.zoom.enabled) === true; + if (evt.keyCode === 38) this.clickUpArrow(); + else if (evt.keyCode === 40) this.clickDownArrow(); + else if (evt.keyCode === 37) this.clickLeftArrow(); + else if (evt.keyCode === 39) this.clickRightArrow(); + else if (doZoom && evt.keyCode === 171) this.zoomIn(); + else if (doZoom && evt.keyCode === 173) this.zoomOut(); + else if (doZoom && evt.keyCode === 79) this.zoom100(); + else done = false; + } else if (!evt.shiftKey && !evt.ctrlKey) { + let dx = JXG.evaluate(this.attr.keyboard.dx) / this.unitX; + let dy = JXG.evaluate(this.attr.keyboard.dy) / this.unitY; + if (JXG.exists(el.visProp)) { + if ( + JXG.exists(el.visProp.snaptogrid) && + el.visProp.snaptogrid && + el.evalVisProp('snapsizex') && + el.evalVisProp('snapsizey') + ) { + const res = el.getSnapSizes(); + dx = res[0]; + dy = res[1]; + } else if ( + JXG.exists(el.visProp.attracttogrid) && + el.visProp.attracttogrid && + el.evalVisProp('attractordistance') && + el.evalVisProp('attractorunit') + ) { + let sX = 1.1 * el.evalVisProp('attractordistance'); + let sY = sX; + if (el.evalVisProp('attractorunit') === 'screen') { + sX /= this.unitX; + sY /= this.unitX; + } + dx = Math.max(sX, dx); + dy = Math.max(sY, dy); + } + } + + let dir; + if (evt.keyCode === 38) dir = [0, dy]; + else if (evt.keyCode === 40) dir = [0, -dy]; + else if (evt.keyCode === 37) dir = [-dx, 0]; + else if (evt.keyCode === 39) dir = [dx, 0]; + else done = false; + + if ( + dir && + el.isDraggable && + el.visPropCalc.visible && + ((this.geonextCompatibilityMode && + (JXG.isPoint(el) || el.elementClass === Const.OBJECT_CLASS_TEXT)) || + !this.geonextCompatibilityMode) && + !el.evalVisProp('fixed') + ) { + this.mode = this.BOARD_MODE_DRAG; + if (JXG.exists(el.coords)) { + const actPos = el.coords.usrCoords.slice(1); + dir[0] += actPos[0]; + dir[1] += actPos[1]; + el.setPosition(JXG.COORDS_BY_USER, dir); + this.updateInfobox(el); + } else { + this.displayInfobox(false); + el.setPositionDirectly(Const.COORDS_BY_USER, dir, [0, 0]); + } + + this.triggerEventHandlers(['keymove', 'move'], [evt, this.mode]); + el.triggerEventHandlers(['keydrag', 'drag'], [evt]); + this.mode = this.BOARD_MODE_NONE; + } + } else if (evt.key === 'Tab') { + done = false; + } + + this.update(); + + if (done && JXG.exists(evt.preventDefault)) evt.preventDefault(); + return done; + }; + gt.board.addKeyboardEventHandlers(); + gt.graphContainer.tabIndex = -1; gt.board.containerObj.tabIndex = -1; @@ -805,7 +913,7 @@ window.graphTool = (containerId, options) => { const point = gt.board.create('point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], { snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - ...gt.definingPointAttributes + ...gt.definingPointAttributes() }); point.setAttribute({ snapToGrid: true }); if (!gt.isStatic) { diff --git a/htdocs/js/GraphTool/intervaltools.js b/htdocs/js/GraphTool/intervaltools.js index 8839e82dcb..8fa9a720cb 100644 --- a/htdocs/js/GraphTool/intervaltools.js +++ b/htdocs/js/GraphTool/intervaltools.js @@ -454,7 +454,8 @@ highlightStrokeWidth: 3, highlightStrokeColor: gt.color.pointHighlightDarker, // highlightFillColor is gt.color.pointHighlight if not included. - highlightFillColor: gt.color.pointHighlightDarker + highlightFillColor: gt.color.pointHighlightDarker, + tabindex: gt.isStatic ? -1 : 0 }; } diff --git a/htdocs/js/GraphTool/linetool.js b/htdocs/js/GraphTool/linetool.js index acf6cb03c1..9e97d1a3c3 100644 --- a/htdocs/js/GraphTool/linetool.js +++ b/htdocs/js/GraphTool/linetool.js @@ -188,7 +188,7 @@ const point1 = this.point1; delete this.point1; - point1.setAttribute(gt.definingPointAttributes); + point1.setAttribute(gt.definingPointAttributes()); point1.on('down', () => gt.onPointDown(point1)); point1.on('up', () => gt.onPointUp(point1)); diff --git a/htdocs/js/GraphTool/parabolatool.js b/htdocs/js/GraphTool/parabolatool.js index c1da9bb0da..e0d65cce5c 100644 --- a/htdocs/js/GraphTool/parabolatool.js +++ b/htdocs/js/GraphTool/parabolatool.js @@ -237,7 +237,7 @@ const vertex = this.vertex; delete this.vertex; - vertex.setAttribute(gt.definingPointAttributes); + vertex.setAttribute(gt.definingPointAttributes()); vertex.on('down', () => gt.onPointDown(vertex)); vertex.on('up', () => gt.onPointUp(vertex)); diff --git a/htdocs/js/GraphTool/pointtool.js b/htdocs/js/GraphTool/pointtool.js index 2420b41a24..eea92d4fde 100644 --- a/htdocs/js/GraphTool/pointtool.js +++ b/htdocs/js/GraphTool/pointtool.js @@ -22,7 +22,8 @@ strokeColor: gt.color.curve, fixed: gt.isStatic, highlightStrokeColor: gt.color.underConstruction, - highlightFillColor: gt.color.pointHighlight + highlightFillColor: gt.color.pointHighlight, + tabindex: gt.isStatic ? -1 : 0 }) ); diff --git a/htdocs/js/GraphTool/quadratictool.js b/htdocs/js/GraphTool/quadratictool.js index a5a8d94203..bef7eb2893 100644 --- a/htdocs/js/GraphTool/quadratictool.js +++ b/htdocs/js/GraphTool/quadratictool.js @@ -12,7 +12,7 @@ constructor(point1, point2, point3, solid) { for (const point of [point1, point2, point3]) { - point.setAttribute(gt.definingPointAttributes); + point.setAttribute(gt.definingPointAttributes()); if (!gt.isStatic) { point.on('down', () => gt.onPointDown(point)); point.on('up', () => gt.onPointUp(point)); diff --git a/htdocs/js/GraphTool/quadrilateral.js b/htdocs/js/GraphTool/quadrilateral.js index 9ec263626b..b9211b9680 100644 --- a/htdocs/js/GraphTool/quadrilateral.js +++ b/htdocs/js/GraphTool/quadrilateral.js @@ -12,7 +12,7 @@ constructor(point1, point2, point3, point4, solid) { for (const point of [point1, point2, point3, point4]) { - point.setAttribute(gt.definingPointAttributes); + point.setAttribute(gt.definingPointAttributes()); if (!gt.isStatic) { point.on('down', () => gt.onPointDown(point)); point.on('up', () => gt.onPointUp(point)); diff --git a/htdocs/js/GraphTool/sinewavetool.js b/htdocs/js/GraphTool/sinewavetool.js index 057559d9f5..a46e597a13 100644 --- a/htdocs/js/GraphTool/sinewavetool.js +++ b/htdocs/js/GraphTool/sinewavetool.js @@ -12,7 +12,7 @@ constructor(shiftPoint, periodPoint, amplitudePoint, solid) { for (const point of [shiftPoint, periodPoint, amplitudePoint]) { - point.setAttribute(gt.definingPointAttributes); + point.setAttribute(gt.definingPointAttributes()); if (!gt.isStatic) { point.on('down', () => gt.onPointDown(point)); point.on('up', () => gt.onPointUp(point)); diff --git a/htdocs/js/GraphTool/triangle.js b/htdocs/js/GraphTool/triangle.js index e68f15aea6..757b5eb665 100644 --- a/htdocs/js/GraphTool/triangle.js +++ b/htdocs/js/GraphTool/triangle.js @@ -12,7 +12,7 @@ constructor(point1, point2, point3, solid) { for (const point of [point1, point2, point3]) { - point.setAttribute(gt.definingPointAttributes); + point.setAttribute(gt.definingPointAttributes()); if (!gt.isStatic) { point.on('down', () => gt.onPointDown(point)); point.on('up', () => gt.onPointUp(point)); diff --git a/htdocs/package-lock.json b/htdocs/package-lock.json index 8e070015f2..9c40f83d17 100644 --- a/htdocs/package-lock.json +++ b/htdocs/package-lock.json @@ -8,7 +8,7 @@ "license": "GPL-2.0+", "dependencies": { "@openwebwork/mathquill": "^0.11.1", - "jsxgraph": "^1.11.1", + "jsxgraph": "^1.12.2", "jszip": "^3.10.1", "jszip-utils": "^0.1.0", "plotly.js-dist-min": "^3.1.0", @@ -1026,9 +1026,9 @@ "license": "MIT" }, "node_modules/jsxgraph": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.11.1.tgz", - "integrity": "sha512-0UdVqQPrKiHH29QZq0goaJvJ6eAAHln00/9urKyiTgqqFWA0xX4/akUbaz9N5cmdh8fQ6NPSwMe43TbeAWQfXA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.12.2.tgz", + "integrity": "sha512-7kTscexFVBHirsBrxZFQg2hA6Mf/Pa2piojNgxHZ9i/rQfxDAaq7p8oD9/009clQTFQ9fNLUpmDKwV+84zA2Gg==", "license": "(MIT OR LGPL-3.0-or-later)", "dependencies": { "jsxgraph": "^1.11.0-beta2" @@ -2622,9 +2622,9 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "jsxgraph": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.11.1.tgz", - "integrity": "sha512-0UdVqQPrKiHH29QZq0goaJvJ6eAAHln00/9urKyiTgqqFWA0xX4/akUbaz9N5cmdh8fQ6NPSwMe43TbeAWQfXA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.12.2.tgz", + "integrity": "sha512-7kTscexFVBHirsBrxZFQg2hA6Mf/Pa2piojNgxHZ9i/rQfxDAaq7p8oD9/009clQTFQ9fNLUpmDKwV+84zA2Gg==", "requires": { "jsxgraph": "^1.11.0-beta2" } diff --git a/htdocs/package.json b/htdocs/package.json index f4cfd4ec1d..6de2eed492 100644 --- a/htdocs/package.json +++ b/htdocs/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@openwebwork/mathquill": "^0.11.1", - "jsxgraph": "^1.11.1", + "jsxgraph": "^1.12.2", "jszip": "^3.10.1", "jszip-utils": "^0.1.0", "plotly.js-dist-min": "^3.1.0",