jsxgraph 1.3.0 → 1.4.1

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.
Files changed (90) hide show
  1. package/README.md +2 -2
  2. package/distrib/README.md +2 -2
  3. package/distrib/geogebra.min.js +3 -3
  4. package/distrib/geonext.min.js +3 -3
  5. package/distrib/index.d.ts +651 -448
  6. package/distrib/intergeo.min.js +3 -3
  7. package/distrib/jsxgraph.css +4 -13
  8. package/distrib/jsxgraphcore.js +9 -25
  9. package/distrib/jsxgraphsrc.js +28203 -25234
  10. package/distrib/sketch.min.js +3 -3
  11. package/package.json +11 -22
  12. package/src/base/board.js +674 -469
  13. package/src/base/chart.js +1 -1
  14. package/src/base/circle.js +23 -1
  15. package/src/base/composition.js +1 -1
  16. package/src/base/constants.js +32 -11
  17. package/src/base/coords.js +4 -4
  18. package/src/base/coordselement.js +51 -24
  19. package/src/base/curve.js +308 -117
  20. package/src/base/element.js +79 -50
  21. package/src/base/foreignobject.js +463 -0
  22. package/src/base/group.js +2 -2
  23. package/src/base/image.js +12 -15
  24. package/src/base/line.js +108 -21
  25. package/src/base/point.js +24 -28
  26. package/src/base/polygon.js +75 -18
  27. package/src/base/text.js +116 -63
  28. package/src/base/ticks.js +29 -11
  29. package/src/base/transformation.js +1 -1
  30. package/src/base/turtle.js +1 -1
  31. package/src/element/arc.js +1 -1
  32. package/src/element/button.js +29 -2
  33. package/src/element/checkbox.js +31 -1
  34. package/src/element/comb.js +5 -2
  35. package/src/element/composition.js +127 -19
  36. package/src/element/conic.js +46 -5
  37. package/src/element/input.js +32 -1
  38. package/src/element/locus.js +2 -2
  39. package/src/element/measure.js +9 -4
  40. package/src/element/sector.js +46 -12
  41. package/src/element/slider.js +17 -12
  42. package/src/element/slopetriangle.js +1 -1
  43. package/src/jsxgraph.js +28 -9
  44. package/src/jxg.js +4 -3
  45. package/src/loadjsxgraph.js +3 -2
  46. package/src/math/bst.js +1 -1
  47. package/src/math/clip.js +550 -345
  48. package/src/math/complex.js +1 -1
  49. package/src/math/extrapolate.js +1 -1
  50. package/src/math/geometry.js +155 -126
  51. package/src/math/ia.js +290 -21
  52. package/src/math/math.js +30 -1
  53. package/src/math/metapost.js +1 -1
  54. package/src/math/nlp.js +25 -5
  55. package/src/math/numerics.js +183 -4
  56. package/src/math/plot.js +4 -4
  57. package/src/math/poly.js +1 -1
  58. package/src/math/probfuncs.js +2 -1
  59. package/src/math/qdt.js +68 -62
  60. package/src/math/statistics.js +58 -7
  61. package/src/math/symbolic.js +1 -1
  62. package/src/options.js +377 -114
  63. package/src/parser/ca.js +1297 -1291
  64. package/src/parser/datasource.js +1 -1
  65. package/src/parser/geonext.js +6 -6
  66. package/src/parser/jessiecode.js +275 -79
  67. package/src/parser/jessiescript.js +1 -1
  68. package/src/reader/cinderella.js +1 -1
  69. package/src/reader/file.js +1 -1
  70. package/src/reader/geogebra.js +1 -1
  71. package/src/reader/geonext.js +1 -1
  72. package/src/reader/graph.js +1 -1
  73. package/src/reader/intergeo.js +1 -1
  74. package/src/reader/sketch.js +21 -13
  75. package/src/renderer/abstract.js +21 -6
  76. package/src/renderer/canvas.js +22 -15
  77. package/src/renderer/no.js +11 -4
  78. package/src/renderer/svg.js +76 -53
  79. package/src/renderer/vml.js +36 -32
  80. package/src/server/server.js +1 -1
  81. package/src/utils/base64.js +2 -2
  82. package/src/utils/color.js +148 -3
  83. package/src/utils/dump.js +21 -15
  84. package/src/utils/env.js +63 -104
  85. package/src/utils/event.js +1 -1
  86. package/src/utils/expect.js +1 -1
  87. package/src/utils/type.js +30 -19
  88. package/src/utils/xml.js +1 -1
  89. package/src/utils/zip.js +1 -1
  90. package/CHANGELOG.md +0 -1281
package/src/base/board.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2008-2021
2
+ Copyright 2008-2022
3
3
  Matthias Ehmann,
4
4
  Michael Gerhaeuser,
5
5
  Carsten Miller,
@@ -65,10 +65,10 @@
65
65
  define([
66
66
  'jxg', 'base/constants', 'base/coords', 'options', 'math/numerics', 'math/math', 'math/geometry', 'math/complex',
67
67
  'math/statistics',
68
- 'parser/jessiecode', 'parser/geonext', 'utils/color', 'utils/type', 'utils/event', 'utils/env', 'base/transformation',
69
- 'base/point', 'base/line', 'base/text', 'element/composition', 'base/composition'
70
- ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, GeonextParser, Color, Type,
71
- EventEmitter, Env, Transform, Point, Line, Text, Composition, EComposition) {
68
+ 'parser/jessiecode', 'utils/color', 'utils/type', 'utils/event', 'utils/env',
69
+ 'base/composition'
70
+ ], function (JXG, Const, Coords, Options, Numerics, Mat, Geometry, Complex, Statistics, JessieCode, Color, Type,
71
+ EventEmitter, Env, Composition) {
72
72
 
73
73
  'use strict';
74
74
 
@@ -120,20 +120,20 @@ define([
120
120
  this.BOARD_MODE_MOVE_ORIGIN = 0x0002;
121
121
 
122
122
  /**
123
- * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
123
+ * Update is made with high quality, e.g. graphs are evaluated at much more points.
124
124
  * @type Number
125
125
  * @constant
126
126
  * @see JXG.Board#updateQuality
127
127
  */
128
- this.BOARD_QUALITY_LOW = 0x1;
128
+ this.BOARD_MODE_ZOOM = 0x0011;
129
129
 
130
130
  /**
131
- * Update is made with high quality, e.g. graphs are evaluated at much more points.
131
+ * Update is made with low quality, e.g. graphs are evaluated at a lesser amount of points.
132
132
  * @type Number
133
133
  * @constant
134
134
  * @see JXG.Board#updateQuality
135
135
  */
136
- this.BOARD_QUALITY_HIGH = 0x2;
136
+ this.BOARD_QUALITY_LOW = 0x1;
137
137
 
138
138
  /**
139
139
  * Update is made with high quality, e.g. graphs are evaluated at much more points.
@@ -141,7 +141,7 @@ define([
141
141
  * @constant
142
142
  * @see JXG.Board#updateQuality
143
143
  */
144
- this.BOARD_MODE_ZOOM = 0x0011;
144
+ this.BOARD_QUALITY_HIGH = 0x2;
145
145
 
146
146
  /**
147
147
  * Pointer to the document element containing the board.
@@ -174,11 +174,16 @@ define([
174
174
  /**
175
175
  * A reference to this boards renderer.
176
176
  * @type JXG.AbstractRenderer
177
+ * @name JXG.Board#renderer
178
+ * @private
179
+ * @ignore
177
180
  */
178
181
  this.renderer = renderer;
179
182
 
180
183
  /**
181
184
  * Grids keeps track of all grids attached to this board.
185
+ * @type Array
186
+ * @private
182
187
  */
183
188
  this.grids = [];
184
189
 
@@ -204,6 +209,7 @@ define([
204
209
  * usrCoords and scrCoords. usrCoords always equals [1, 0, 0] and scrCoords
205
210
  * stores the boards origin in homogeneous screen coordinates.
206
211
  * @type Object
212
+ * @private
207
213
  */
208
214
  this.origin = {};
209
215
  this.origin.usrCoords = [1, 0, 0];
@@ -212,44 +218,56 @@ define([
212
218
  /**
213
219
  * Zoom factor in X direction. It only stores the zoom factor to be able
214
220
  * to get back to 100% in zoom100().
221
+ * @name JXG.Board.zoomX
215
222
  * @type Number
223
+ * @private
224
+ * @ignore
216
225
  */
217
226
  this.zoomX = zoomX;
218
227
 
219
228
  /**
220
229
  * Zoom factor in Y direction. It only stores the zoom factor to be able
221
230
  * to get back to 100% in zoom100().
231
+ * @name JXG.Board.zoomY
222
232
  * @type Number
233
+ * @private
234
+ * @ignore
223
235
  */
224
236
  this.zoomY = zoomY;
225
237
 
226
238
  /**
227
239
  * The number of pixels which represent one unit in user-coordinates in x direction.
228
240
  * @type Number
241
+ * @private
229
242
  */
230
243
  this.unitX = unitX * this.zoomX;
231
244
 
232
245
  /**
233
246
  * The number of pixels which represent one unit in user-coordinates in y direction.
234
247
  * @type Number
248
+ * @private
235
249
  */
236
250
  this.unitY = unitY * this.zoomY;
237
251
 
238
252
  /**
239
253
  * Keep aspect ratio if bounding box is set and the width/height ratio differs from the
240
254
  * width/height ratio of the canvas.
255
+ * @type Boolean
256
+ * @private
241
257
  */
242
258
  this.keepaspectratio = false;
243
259
 
244
260
  /**
245
261
  * Canvas width.
246
262
  * @type Number
263
+ * @private
247
264
  */
248
265
  this.canvasWidth = canvasWidth;
249
266
 
250
267
  /**
251
268
  * Canvas Height
252
269
  * @type Number
270
+ * @private
253
271
  */
254
272
  this.canvasHeight = canvasHeight;
255
273
 
@@ -379,15 +397,15 @@ define([
379
397
 
380
398
  /**
381
399
  * References to the object that is dragged with the mouse on the board.
382
- * @type {@link JXG.GeometryElement}.
383
- * @see {JXG.Board#touches}
400
+ * @type JXG.GeometryElement
401
+ * @see JXG.Board#touches
384
402
  */
385
403
  this.mouse = {};
386
404
 
387
405
  /**
388
406
  * Keeps track on touched elements, like {@link JXG.Board#mouse} does for mouse events.
389
407
  * @type Array
390
- * @see {JXG.Board#mouse}
408
+ * @see JXG.Board#mouse
391
409
  */
392
410
  this.touches = [];
393
411
 
@@ -412,6 +430,13 @@ define([
412
430
  */
413
431
  this.touchMoveLast = 0;
414
432
 
433
+ /**
434
+ * Contains the pointerId of the last touchMove event which was not thrown away or since
435
+ * touchStart because Android's Webkit browser fires too much of them.
436
+ * @type Number
437
+ */
438
+ this.touchMoveLastId = Infinity;
439
+
415
440
  /**
416
441
  * Contains the last time (epoch, msec) since the last getCoordsTopLeftCorner call which was not thrown away.
417
442
  * @type Number
@@ -482,7 +507,7 @@ define([
482
507
 
483
508
  /**
484
509
  * A flag which stores if the board registered pointer events.
485
- * @type {Boolean}
510
+ * @type Boolean
486
511
  * @default false
487
512
  */
488
513
  this.hasPointerHandlers = false;
@@ -503,14 +528,14 @@ define([
503
528
 
504
529
  /**
505
530
  * A flag which tells us if the board has a pointerUp event registered at the moment.
506
- * @type {Boolean}
531
+ * @type Boolean
507
532
  * @default false
508
533
  */
509
534
  this.hasPointerUp = false;
510
535
 
511
536
  /**
512
537
  * Offset for large coords elements like images
513
- * @type {Array}
538
+ * @type Array
514
539
  * @private
515
540
  * @default [0, 0]
516
541
  */
@@ -518,31 +543,36 @@ define([
518
543
 
519
544
  /**
520
545
  * Stores the input device used in the last down or move event.
521
- * @type {String}
546
+ * @type String
522
547
  * @private
523
548
  * @default 'mouse'
524
549
  */
525
550
  this._inputDevice = 'mouse';
526
551
 
552
+ /**
553
+ * Keeps a list of pointer devices which are currently touching the screen.
554
+ * @type Array
555
+ * @private
556
+ */
527
557
  this._board_touches = [];
528
558
 
529
559
  /**
530
560
  * A flag which tells us if the board is in the selecting mode
531
- * @type {Boolean}
561
+ * @type Boolean
532
562
  * @default false
533
563
  */
534
564
  this.selectingMode = false;
535
565
 
536
566
  /**
537
567
  * A flag which tells us if the user is selecting
538
- * @type {Boolean}
568
+ * @type Boolean
539
569
  * @default false
540
570
  */
541
571
  this.isSelecting = false;
542
572
 
543
573
  /**
544
574
  * A flag which tells us if the user is scrolling the viewport
545
- * @type {Boolean}
575
+ * @type Boolean
546
576
  * @private
547
577
  * @default false
548
578
  * @see JXG.Board#scrollListener
@@ -551,7 +581,7 @@ define([
551
581
 
552
582
  /**
553
583
  * A flag which tells us if a resize is in process
554
- * @type {Boolean}
584
+ * @type Boolean
555
585
  * @private
556
586
  * @default false
557
587
  * @see JXG.Board#resizeListener
@@ -560,7 +590,7 @@ define([
560
590
 
561
591
  /**
562
592
  * A bounding box for the selection
563
- * @type {Array}
593
+ * @type Array
564
594
  * @default [ [0,0], [0,0] ]
565
595
  */
566
596
  this.selectingBox = [[0, 0], [0, 0]];
@@ -764,17 +794,20 @@ define([
764
794
  * @see JXG.Board#mouseMoveListener
765
795
  */
766
796
  checkFrameRate: function(evt) {
767
- var time = new Date().getTime();
797
+ var handleEvt = false,
798
+ time = new Date().getTime();
768
799
 
769
- if ((time - this.touchMoveLast) * this.attr.maxframerate < 1000) {
770
- // this.updateQuality = this.BOARD_QUALITY_HIGH;
771
- // this.triggerEventHandlers(['touchmove', 'move'], [evt, this.mode]);
772
-
773
- return false;
800
+ if (Type.exists(evt.pointerId) && this.touchMoveLastId !== evt.pointerId) {
801
+ handleEvt = true;
802
+ this.touchMoveLastId = evt.pointerId;
774
803
  }
775
-
776
- this.touchMoveLast = time;
777
- return true;
804
+ if (!handleEvt && (time - this.touchMoveLast) * this.attr.maxframerate >= 1000) {
805
+ handleEvt = true;
806
+ }
807
+ if (handleEvt) {
808
+ this.touchMoveLast = time;
809
+ }
810
+ return handleEvt;
778
811
  },
779
812
 
780
813
  /**
@@ -903,7 +936,7 @@ define([
903
936
  absPos,
904
937
  v;
905
938
 
906
- // position of mouse cursor relative to containers position of container
939
+ // Position of cursor using clientX/Y
907
940
  absPos = Env.getPosition(e, i, this.document);
908
941
 
909
942
  /**
@@ -912,6 +945,7 @@ define([
912
945
  if (!Type.exists(this.cssTransMat)) {
913
946
  this.updateCSSTransforms();
914
947
  }
948
+ // Position relative to the top left corner
915
949
  v = [1, absPos[0] - cPos[0], absPos[1] - cPos[1]];
916
950
  v = Mat.matVecMult(this.cssTransMat, v);
917
951
  v[1] /= v[0];
@@ -1129,8 +1163,8 @@ define([
1129
1163
  * @param {Object} o The touch object that is dragged: {JXG.Board#touches}.
1130
1164
  * @param {Object} evt The event object that lead to this movement.
1131
1165
  */
1132
- twoFingerMove: function (p1, p2, o, evt) {
1133
- var np1c, np2c, drag;
1166
+ twoFingerMove: function (o, id, evt) {
1167
+ var drag;
1134
1168
 
1135
1169
  if (Type.exists(o) && Type.exists(o.obj)) {
1136
1170
  drag = o.obj;
@@ -1138,60 +1172,51 @@ define([
1138
1172
  return;
1139
1173
  }
1140
1174
 
1141
- // New finger position
1142
- np1c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p1[0], p1[1]), this);
1143
- np2c = new Coords(Const.COORDS_BY_SCREEN, this.getScrCoordsOfMouse(p2[0], p2[1]), this);
1144
-
1145
1175
  if (drag.elementClass === Const.OBJECT_CLASS_LINE ||
1146
- drag.type === Const.OBJECT_TYPE_POLYGON) {
1147
- this.twoFingerTouchObject(np1c, np2c, o, drag, evt);
1176
+ drag.type === Const.OBJECT_TYPE_POLYGON) {
1177
+ this.twoFingerTouchObject(o.targets, drag, id);
1148
1178
  } else if (drag.elementClass === Const.OBJECT_CLASS_CIRCLE) {
1149
- this.twoFingerTouchCircle(np1c, np2c, o, drag);
1179
+ this.twoFingerTouchCircle(o.targets, drag, id);
1150
1180
  }
1151
1181
 
1152
- drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1153
-
1154
- o.targets[0].Xprev = np1c.scrCoords[1];
1155
- o.targets[0].Yprev = np1c.scrCoords[2];
1156
- o.targets[1].Xprev = np2c.scrCoords[1];
1157
- o.targets[1].Yprev = np2c.scrCoords[2];
1182
+ if (evt) {
1183
+ drag.triggerEventHandlers(['touchdrag', 'drag'], [evt]);
1184
+ }
1158
1185
  },
1159
1186
 
1160
1187
  /**
1161
1188
  * Moves, rotates and scales a line or polygon with two fingers.
1162
- * @param {JXG.Coords} np1c x,y coordinates of first touch
1163
- * @param {JXG.Coords} np2c x,y coordinates of second touch
1164
- * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1189
+ * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1165
1190
  * @param {object} drag The object that is dragged:
1166
- * @param {Object} evt The event object that lead to this movement.
1191
+ * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1167
1192
  */
1168
- twoFingerTouchObject: function (np1c, np2c, o, drag, evt) {
1169
- var np1, np2, op1, op2,
1170
- nmid, omid, nd, od,
1193
+ twoFingerTouchObject: function (tar, drag, id) {
1194
+ var np, op, nd, od,
1171
1195
  d, alpha,
1172
- S, t1, t2, t3, t4, t5,
1196
+ S, t1, t3, t4, t5,
1173
1197
  ar, i, len,
1174
- pi180 = 0.017453292519943295; // Math.PI / 180.0
1198
+ fixEl, moveEl, fix;
1175
1199
 
1176
- if (Type.exists(o.targets[0]) &&
1177
- Type.exists(o.targets[1]) &&
1178
- !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1200
+ if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1201
+ !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1179
1202
 
1180
- // Actual fingers' position
1181
- np1 = np1c.usrCoords;
1182
- np2 = np2c.usrCoords;
1183
-
1184
- // Previous fingers' position
1185
- op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1186
- op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1203
+ if (id === tar[0].num) {
1204
+ fixEl = tar[1];
1205
+ moveEl = tar[0];
1206
+ } else {
1207
+ fixEl = tar[0];
1208
+ moveEl = tar[1];
1209
+ }
1187
1210
 
1188
- // Affine mid points of the old and new positions
1189
- omid = [1, (op1[1] + op2[1]) * 0.5, (op1[2] + op2[2]) * 0.5];
1190
- nmid = [1, (np1[1] + np2[1]) * 0.5, (np1[2] + np2[2]) * 0.5];
1211
+ fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1212
+ // Previous finger position
1213
+ op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1214
+ // New finger position
1215
+ np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1191
1216
 
1192
1217
  // Old and new directions
1193
- od = Mat.crossProduct(op1, op2);
1194
- nd = Mat.crossProduct(np1, np2);
1218
+ od = Mat.crossProduct(fix, op);
1219
+ nd = Mat.crossProduct(fix, np);
1195
1220
 
1196
1221
  // Intersection between the two directions
1197
1222
  S = Mat.crossProduct(od, nd);
@@ -1201,45 +1226,18 @@ define([
1201
1226
  return;
1202
1227
  }
1203
1228
 
1204
- // Normalize the coordinates
1205
- S[1] /= S[0];
1206
- S[2] /= S[0];
1229
+ alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1207
1230
 
1208
- if (Type.exists(evt.rotation) && evt.type !== 'pointermove') {
1209
- // iOS touch events contain the angle for free
1210
- alpha = evt.rotation - this.previousRotation;
1211
- this.previousRotation = evt.rotation;
1212
- alpha *= -pi180;
1213
- } else {
1214
- // For pointer events, the rotation angle has to be calculated.
1215
- alpha = Geometry.rad(omid.slice(1), S.slice(1), nmid.slice(1));
1216
- }
1217
-
1218
- t1 = this.create('transform', [alpha, S[1], S[2]], {type: 'rotate'});
1219
-
1220
- // Old midpoint of fingers after first transformation:
1231
+ t1 = this.create('transform', [alpha, [fix[1], fix[2]]], {type: 'rotate'});
1221
1232
  t1.update();
1222
- omid = Mat.matVecMult(t1.matrix, omid);
1223
- omid[1] /= omid[0];
1224
- omid[2] /= omid[0];
1225
1233
 
1226
- // Shift to the new mid point
1227
- t2 = this.create('transform', [nmid[1] - omid[1], nmid[2] - omid[2]], {type: 'translate'});
1228
- t2.update();
1229
-
1230
- t1.melt(t2);
1231
1234
  if (Type.evaluate(drag.visProp.scalable)) {
1232
1235
  // Scale
1233
- if (Type.exists(evt.scale)) {
1234
- d = evt.scale / this.previousScale;
1235
- this.previousScale = evt.scale;
1236
- } else {
1237
- d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1238
- }
1236
+ d = Geometry.distance(np, fix) / Geometry.distance(op, fix);
1239
1237
 
1240
- t3 = this.create('transform', [-nmid[1], -nmid[2]], {type: 'translate'});
1238
+ t3 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1241
1239
  t4 = this.create('transform', [d, d], {type: 'scale'});
1242
- t5 = this.create('transform', [nmid[1], nmid[2]], {type: 'translate'});
1240
+ t5 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1243
1241
  t1.melt(t3).melt(t4).melt(t5);
1244
1242
  }
1245
1243
 
@@ -1270,46 +1268,48 @@ define([
1270
1268
 
1271
1269
  /*
1272
1270
  * Moves, rotates and scales a circle with two fingers.
1273
- * @param {JXG.Coords} np1c x,y coordinates of first touch
1274
- * @param {JXG.Coords} np2c x,y coordinates of second touch
1275
- * @param {object} o The touch object that is dragged: {JXG.Board#touches}.
1271
+ * @param {Array} tar Array conatining touch event objects: {JXG.Board#touches.targets}.
1276
1272
  * @param {object} drag The object that is dragged:
1273
+ * @param {Number} id pointerId of the event. In case of old touch event this is emulated.
1277
1274
  */
1278
- twoFingerTouchCircle: function (np1c, np2c, o, drag) {
1279
- var np1, np2, op1, op2,
1280
- d, alpha, t1, t2, t3, t4, t5;
1275
+ twoFingerTouchCircle: function (tar, drag, id) {
1276
+ var fixEl, moveEl, np, op, fix,
1277
+ d, alpha, t1, t2, t3, t4;
1281
1278
 
1282
- if (drag.method === 'pointCircle' ||
1283
- drag.method === 'pointLine') {
1279
+ if (drag.method === 'pointCircle' || drag.method === 'pointLine') {
1284
1280
  return;
1285
1281
  }
1286
1282
 
1287
- if (Type.exists(o.targets[0]) &&
1288
- Type.exists(o.targets[1]) &&
1289
- !isNaN(o.targets[0].Xprev + o.targets[0].Yprev + o.targets[1].Xprev + o.targets[1].Yprev)) {
1283
+ if (Type.exists(tar[0]) && Type.exists(tar[1]) &&
1284
+ !isNaN(tar[0].Xprev + tar[0].Yprev + tar[1].Xprev + tar[1].Yprev)) {
1285
+
1286
+ if (id === tar[0].num) {
1287
+ fixEl = tar[1];
1288
+ moveEl = tar[0];
1289
+ } else {
1290
+ fixEl = tar[0];
1291
+ moveEl = tar[1];
1292
+ }
1290
1293
 
1291
- np1 = np1c.usrCoords;
1292
- np2 = np2c.usrCoords;
1294
+ fix = (new Coords(Const.COORDS_BY_SCREEN, [fixEl.Xprev, fixEl.Yprev], this)).usrCoords;
1293
1295
  // Previous finger position
1294
- op1 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[0].Xprev, o.targets[0].Yprev], this)).usrCoords;
1295
- op2 = (new Coords(Const.COORDS_BY_SCREEN, [o.targets[1].Xprev, o.targets[1].Yprev], this)).usrCoords;
1296
+ op = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.Xprev, moveEl.Yprev], this)).usrCoords;
1297
+ // New finger position
1298
+ np = (new Coords(Const.COORDS_BY_SCREEN, [moveEl.X, moveEl.Y], this)).usrCoords;
1296
1299
 
1297
- // Shift by the movement of the first finger
1298
- t1 = this.create('transform', [np1[1] - op1[1], np1[2] - op1[2]], {type: 'translate'});
1299
- alpha = Geometry.rad(op2.slice(1), np1.slice(1), np2.slice(1));
1300
+ alpha = Geometry.rad(op.slice(1), fix.slice(1), np.slice(1));
1300
1301
 
1301
1302
  // Rotate and scale by the movement of the second finger
1302
- t2 = this.create('transform', [-np1[1], -np1[2]], {type: 'translate'});
1303
- t3 = this.create('transform', [alpha], {type: 'rotate'});
1304
- t1.melt(t2).melt(t3);
1305
-
1303
+ t1 = this.create('transform', [-fix[1], -fix[2]], {type: 'translate'});
1304
+ t2 = this.create('transform', [alpha], {type: 'rotate'});
1305
+ t1.melt(t2);
1306
1306
  if (Type.evaluate(drag.visProp.scalable)) {
1307
- d = Geometry.distance(np1, np2) / Geometry.distance(op1, op2);
1308
- t4 = this.create('transform', [d, d], {type: 'scale'});
1309
- t1.melt(t4);
1307
+ d = Geometry.distance(fix, np) / Geometry.distance(fix, op);
1308
+ t3 = this.create('transform', [d, d], {type: 'scale'});
1309
+ t1.melt(t3);
1310
1310
  }
1311
- t5 = this.create('transform', [np1[1], np1[2]], {type: 'translate'});
1312
- t1.melt(t5);
1311
+ t4 = this.create('transform', [fix[1], fix[2]], {type: 'translate'});
1312
+ t1.melt(t4);
1313
1313
 
1314
1314
  if (drag.center.draggable()) {
1315
1315
  t1.applyOnce([drag.center]);
@@ -1460,20 +1460,20 @@ define([
1460
1460
  * @param {Object} evt Event from touchStartListener
1461
1461
  * @return {Boolean} returns if the origin is moved.
1462
1462
  */
1463
- touchOriginMoveStart: function (evt) {
1463
+ touchStartMoveOriginOneFinger: function (evt) {
1464
1464
  var touches = evt[JXG.touchProperty],
1465
- r, pos;
1465
+ conditions, pos;
1466
1466
 
1467
- r = this.attr.pan.enabled &&
1467
+ conditions = this.attr.pan.enabled &&
1468
1468
  !this.attr.pan.needtwofingers &&
1469
1469
  touches.length === 1;
1470
1470
 
1471
- if (r) {
1471
+ if (conditions) {
1472
1472
  pos = this.getMousePosition(evt, 0);
1473
1473
  this.initMoveOrigin(pos[0], pos[1]);
1474
1474
  }
1475
1475
 
1476
- return r;
1476
+ return conditions;
1477
1477
  },
1478
1478
 
1479
1479
  /**
@@ -1714,7 +1714,6 @@ define([
1714
1714
  } else {
1715
1715
  Env.removeEvent(this.containerObj, 'pointerdown', this.pointerDownListener, this);
1716
1716
  Env.removeEvent(moveTarget, 'pointermove', this.pointerMoveListener, this);
1717
- // Env.removeEvent(this.containerObj, 'pointerout', this.pointerOutListener, this);
1718
1717
  }
1719
1718
 
1720
1719
  Env.removeEvent(this.containerObj, 'mousewheel', this.mouseWheelListener, this);
@@ -1722,9 +1721,10 @@ define([
1722
1721
 
1723
1722
  if (this.hasPointerUp) {
1724
1723
  if (window.navigator.msPointerEnabled) { // IE10-
1725
- Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1724
+ Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
1726
1725
  } else {
1727
- Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1726
+ Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
1727
+ Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
1728
1728
  }
1729
1729
  this.hasPointerUp = false;
1730
1730
  }
@@ -1976,30 +1976,19 @@ define([
1976
1976
  return false;
1977
1977
  },
1978
1978
 
1979
+ /*
1980
+ * Pointer events
1981
+ */
1982
+
1979
1983
  /**
1980
- * Fix for Firefox browser: When using a second finger, the
1981
- * touch event for the first finger is sent again.
1982
1984
  *
1983
- * @param {[type]} evt Event object
1985
+ * Check if pointer event is already registered in {@link JXG.Board#_board_touches}.
1986
+ *
1987
+ * @param {Object} evt Event object
1984
1988
  * @return {Boolean} true if down event has already been sent.
1985
1989
  * @private
1986
1990
  */
1987
- _isPointerEventAlreadyThere: function (evt) {
1988
- var i;
1989
-
1990
- for (i = 0; i < this._board_touches.length; i++) {
1991
- if (this._board_touches[i].pointerId === evt.pointerId) {
1992
- return true;
1993
- }
1994
- }
1995
-
1996
- return false;
1997
- },
1998
-
1999
- /**
2000
- * pointer-Events
2001
- */
2002
- _pointerIsTouchRegistered: function(evt) {
1991
+ _isPointerRegistered: function(evt) {
2003
1992
  var i, len = this._board_touches.length;
2004
1993
 
2005
1994
  for (i = 0; i < len; i++) {
@@ -2010,7 +1999,18 @@ define([
2010
1999
  return false;
2011
2000
  },
2012
2001
 
2013
- _pointerAddBoardTouches: function (evt) {
2002
+ /**
2003
+ *
2004
+ * Store the position of a pointer event.
2005
+ * If not yet done, registers a pointer event in {@link JXG.Board#_board_touches}.
2006
+ * Allows to follow the path of that finger on the screen.
2007
+ * Only two simultaneous touches are supported.
2008
+ *
2009
+ * @param {Object} evt Event object
2010
+ * @returns {JXG.Board} Reference to the board
2011
+ * @private
2012
+ */
2013
+ _pointerStorePosition: function (evt) {
2014
2014
  var i, found;
2015
2015
 
2016
2016
  for (i = 0, found = false; i < this._board_touches.length; i++) {
@@ -2022,7 +2022,8 @@ define([
2022
2022
  }
2023
2023
  }
2024
2024
 
2025
- if (!found) {
2025
+ // Restrict the number of simultaneous touches to 2
2026
+ if (!found && this._board_touches.length < 2) {
2026
2027
  this._board_touches.push({
2027
2028
  pointerId: evt.pointerId,
2028
2029
  clientX: evt.clientX,
@@ -2033,7 +2034,15 @@ define([
2033
2034
  return this;
2034
2035
  },
2035
2036
 
2036
- _pointerRemoveBoardTouches: function (evt) {
2037
+ /**
2038
+ * Deregisters a pointer event in {@link JXG.Board#_board_touches}.
2039
+ * It happens if a finger has been lifted from the screen.
2040
+ *
2041
+ * @param {Object} evt Event object
2042
+ * @returns {JXG.Board} Reference to the board
2043
+ * @private
2044
+ */
2045
+ _pointerRemoveTouches: function (evt) {
2037
2046
  var i;
2038
2047
  for (i = 0; i < this._board_touches.length; i++) {
2039
2048
  if (this._board_touches[i].pointerId === evt.pointerId) {
@@ -2045,6 +2054,22 @@ define([
2045
2054
  return this;
2046
2055
  },
2047
2056
 
2057
+ /**
2058
+ * Remove all registered fingers from {@link JXG.Board#_board_touches}.
2059
+ * This might be necessary if too many fingers have been registered.
2060
+ * @returns {JXG.Board} Reference to the board
2061
+ * @private
2062
+ */
2063
+ _pointerClearTouches: function() {
2064
+ if (this._board_touches.length > 0) {
2065
+ this.dehighlightAll();
2066
+ }
2067
+ this.updateQuality = this.BOARD_QUALITY_HIGH;
2068
+ this.mode = this.BOARD_MODE_NONE;
2069
+ this._board_touches = [];
2070
+ this.touches = [];
2071
+ },
2072
+
2048
2073
  /**
2049
2074
  * Determine which input device is used for this action.
2050
2075
  * Possible devices are 'touch', 'pen' and 'mouse'.
@@ -2062,7 +2087,7 @@ define([
2062
2087
  */
2063
2088
  _getPointerInputDevice: function(evt) {
2064
2089
  if (Env.isBrowser) {
2065
- if (evt.pointerType === 'touch' || // New
2090
+ if (evt.pointerType === 'touch' || // New
2066
2091
  (window.navigator.msMaxTouchPoints && // Old
2067
2092
  window.navigator.msMaxTouchPoints > 1)) {
2068
2093
  return 'touch';
@@ -2085,20 +2110,28 @@ define([
2085
2110
  */
2086
2111
  pointerDownListener: function (evt, object) {
2087
2112
  var i, j, k, pos, elements, sel,
2088
- type = 'mouse', // in case of no browser
2113
+ target_obj,
2114
+ type = 'mouse', // Used in case of no browser
2089
2115
  found, target;
2090
2116
 
2091
- // Temporary fix for Firefox pointer events:
2092
- // When using two fingers, the first touch down event is fired again.
2093
- if (!object && this._isPointerEventAlreadyThere(evt)) {
2117
+ // Fix for Firefox browser: When using a second finger, the
2118
+ // touch event for the first finger is sent again.
2119
+ if (!object && this._isPointerRegistered(evt)) {
2094
2120
  return false;
2095
2121
  }
2096
2122
 
2123
+ if (!object && evt.isPrimary) {
2124
+ // First finger down. To be on the safe side this._board_touches is cleared.
2125
+ this._pointerClearTouches();
2126
+ }
2127
+
2097
2128
  if (!this.hasPointerUp) {
2098
2129
  if (window.navigator.msPointerEnabled) { // IE10-
2099
- Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2130
+ Env.addEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2100
2131
  } else {
2101
- Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
2132
+ // 'pointercancel' is fired e.g. if the finger leaves the browser and drags down the system menu on Android
2133
+ Env.addEvent(this.document, 'pointerup', this.pointerUpListener, this);
2134
+ Env.addEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2102
2135
  }
2103
2136
  this.hasPointerUp = true;
2104
2137
  }
@@ -2128,14 +2161,14 @@ define([
2128
2161
  type = this._inputDevice;
2129
2162
  this.options.precision.hasPoint = this.options.precision[type];
2130
2163
 
2131
- // This should be easier than the touch events. Every pointer device gets its own pointerId, e.g. the mouse
2132
- // always has id 1, fingers and pens get unique ids every time a pointerDown event is fired and they will
2164
+ // Handling of multi touch with pointer events should be easier than the touch events.
2165
+ // Every pointer device has its own pointerId, e.g. the mouse
2166
+ // always has id 1 or 0, fingers and pens get unique ids every time a pointerDown event is fired and they will
2133
2167
  // keep this id until a pointerUp event is fired. What we have to do here is:
2134
2168
  // 1. collect all elements under the current pointer
2135
2169
  // 2. run through the touches control structure
2136
2170
  // a. look for the object collected in step 1.
2137
2171
  // b. if an object is found, check the number of pointers. If appropriate, add the pointer.
2138
-
2139
2172
  pos = this.getMousePosition(evt);
2140
2173
 
2141
2174
  // selection
@@ -2153,45 +2186,41 @@ define([
2153
2186
  elements = this.initMoveObject(pos[0], pos[1], evt, type);
2154
2187
  }
2155
2188
 
2189
+ target_obj = {
2190
+ num: evt.pointerId,
2191
+ X: pos[0],
2192
+ Y: pos[1],
2193
+ Xprev: NaN,
2194
+ Yprev: NaN,
2195
+ Xstart: [],
2196
+ Ystart: [],
2197
+ Zstart: []
2198
+ };
2199
+
2156
2200
  // If no draggable object can be found, get out here immediately
2157
2201
  if (elements.length > 0) {
2158
2202
  // check touches structure
2159
2203
  target = elements[elements.length - 1];
2160
2204
  found = false;
2205
+
2206
+ // Reminder: this.touches is the list of elements which
2207
+ // currently "possess" a pointer (mouse, pen, finger)
2161
2208
  for (i = 0; i < this.touches.length; i++) {
2162
- // the target is already in our touches array, try to add the pointer to the existing touch
2209
+ // An element receives a further touch, i.e.
2210
+ // the target is already in our touches array, add the pointer to the existing touch
2163
2211
  if (this.touches[i].obj === target) {
2164
2212
  j = i;
2165
- k = this.touches[i].targets.push({
2166
- num: evt.pointerId,
2167
- X: pos[0],
2168
- Y: pos[1],
2169
- Xprev: NaN,
2170
- Yprev: NaN,
2171
- Xstart: [],
2172
- Ystart: [],
2173
- Zstart: []
2174
- }) - 1;
2175
-
2213
+ k = this.touches[i].targets.push(target_obj) - 1;
2176
2214
  found = true;
2177
2215
  break;
2178
2216
  }
2179
2217
  }
2180
-
2181
2218
  if (!found) {
2219
+ // An new element hae been touched.
2182
2220
  k = 0;
2183
2221
  j = this.touches.push({
2184
2222
  obj: target,
2185
- targets: [{
2186
- num: evt.pointerId,
2187
- X: pos[0],
2188
- Y: pos[1],
2189
- Xprev: NaN,
2190
- Yprev: NaN,
2191
- Xstart: [],
2192
- Ystart: [],
2193
- Zstart: []
2194
- }]
2223
+ targets: [target_obj]
2195
2224
  }) - 1;
2196
2225
  }
2197
2226
 
@@ -2215,19 +2244,29 @@ define([
2215
2244
  evt.stopPropagation();
2216
2245
  }
2217
2246
 
2218
- if (Env.isBrowser && this._getPointerInputDevice(evt) !== 'touch') {
2247
+ if (!Env.isBrowser) {
2248
+ return false;
2249
+ }
2250
+ if (this._getPointerInputDevice(evt) !== 'touch') {
2219
2251
  if (this.mode === this.BOARD_MODE_NONE) {
2220
2252
  this.mouseOriginMoveStart(evt);
2221
2253
  }
2222
2254
  } else {
2223
- this._pointerAddBoardTouches(evt);
2255
+ this._pointerStorePosition(evt);
2224
2256
  evt.touches = this._board_touches;
2225
2257
 
2226
- // See touchStartListener
2227
- if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) {
2228
- } else if ((this.mode === this.BOARD_MODE_NONE ||
2229
- this.mode === this.BOARD_MODE_MOVE_ORIGIN) &&
2230
- evt.touches.length == 2) {
2258
+ // Touch events on empty areas of the board are handled here, see also touchStartListener
2259
+ // 1. case: one finger. If allowed, this triggers pan with one finger
2260
+ if (evt.touches.length === 1 &&
2261
+ this.mode === this.BOARD_MODE_NONE &&
2262
+ this.touchStartMoveOriginOneFinger(evt)) {
2263
+ // Empty by purpose
2264
+ } else if (evt.touches.length === 2 &&
2265
+ (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2266
+ ) {
2267
+ // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2268
+ // This happens when the second finger hits the device. First, the
2269
+ // "one finger pan mode" has to be cancelled.
2231
2270
  if (this.mode === this.BOARD_MODE_MOVE_ORIGIN) {
2232
2271
  this.originMoveEnd();
2233
2272
  }
@@ -2240,20 +2279,20 @@ define([
2240
2279
  return false;
2241
2280
  },
2242
2281
 
2243
- /**
2244
- * Called if pointer leaves an HTML tag. Is called by the inner-most tag.
2245
- * That means, if a JSXGraph text, i.e. an HTML div, is placed close
2246
- * to the border of the board, this pointerout event will be ignored.
2247
- * @param {Event} evt
2248
- * @return {Boolean}
2249
- */
2250
- pointerOutListener: function (evt) {
2251
- if (evt.target === this.containerObj ||
2252
- (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
2253
- this.pointerUpListener(evt);
2254
- }
2255
- return this.mode === this.BOARD_MODE_NONE;
2256
- },
2282
+ // /**
2283
+ // * Called if pointer leaves an HTML tag. It is called by the inner-most tag.
2284
+ // * That means, if a JSXGraph text, i.e. an HTML div, is placed close
2285
+ // * to the border of the board, this pointerout event will be ignored.
2286
+ // * @param {Event} evt
2287
+ // * @return {Boolean}
2288
+ // */
2289
+ // pointerOutListener: function (evt) {
2290
+ // if (evt.target === this.containerObj ||
2291
+ // (this.renderer.type === 'svg' && evt.target === this.renderer.foreignObjLayer)) {
2292
+ // this.pointerUpListener(evt);
2293
+ // }
2294
+ // return this.mode === this.BOARD_MODE_NONE;
2295
+ // },
2257
2296
 
2258
2297
  /**
2259
2298
  * Called periodically by the browser while the user moves a pointing device across the screen.
@@ -2261,10 +2300,10 @@ define([
2261
2300
  * @returns {Boolean}
2262
2301
  */
2263
2302
  pointerMoveListener: function (evt) {
2264
- var i, j, pos,
2303
+ var i, j, pos, touchTargets,
2265
2304
  type = 'mouse'; // in case of no browser
2266
2305
 
2267
- if (this._getPointerInputDevice(evt) === 'touch' && !this._pointerIsTouchRegistered(evt)) {
2306
+ if (this._getPointerInputDevice(evt) === 'touch' && !this._isPointerRegistered(evt)) {
2268
2307
  // Test, if there was a previous down event of this _getPointerId
2269
2308
  // (in case it is a touch event).
2270
2309
  // Otherwise this move event is ignored. This is necessary e.g. for sketchometry.
@@ -2298,45 +2337,37 @@ define([
2298
2337
  this.triggerEventHandlers(['touchmoveselecting', 'moveselecting', 'pointermoveselecting'], [evt, this.mode]);
2299
2338
  } else if (!this.mouseOriginMove(evt)) {
2300
2339
  if (this.mode === this.BOARD_MODE_DRAG) {
2301
- // Runs through all jsxgraph elements which are touched by at least one finger.
2340
+ // Run through all jsxgraph elements which are touched by at least one finger.
2302
2341
  for (i = 0; i < this.touches.length; i++) {
2342
+ touchTargets = this.touches[i].targets;
2303
2343
  // Run through all touch events which have been started on this jsxgraph element.
2304
- for (j = 0; j < this.touches[i].targets.length; j++) {
2305
- if (this.touches[i].targets[j].num === evt.pointerId) {
2306
- if (this.touches[i].targets.length === 1) {
2307
-
2308
- // Touch by one finger: this is possible for all elements that can be dragged
2309
- this.touches[i].targets[j].X = evt.pageX;
2310
- this.touches[i].targets[j].Y = evt.pageY;
2311
- pos = this.getMousePosition(evt);
2312
- this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2344
+ for (j = 0; j < touchTargets.length; j++) {
2345
+ if (touchTargets[j].num === evt.pointerId) {
2313
2346
 
2314
- } else if (this.touches[i].targets.length === 2) {
2347
+ pos = this.getMousePosition(evt);
2348
+ touchTargets[j].X = pos[0];
2349
+ touchTargets[j].Y = pos[1];
2315
2350
 
2351
+ if (touchTargets.length === 1) {
2352
+ // Touch by one finger: this is possible for all elements that can be dragged
2353
+ this.moveObject(pos[0], pos[1], this.touches[i], evt, type);
2354
+ } else if (touchTargets.length === 2) {
2316
2355
  // Touch by two fingers: e.g. moving lines
2317
- this.touches[i].targets[j].X = evt.pageX;
2318
- this.touches[i].targets[j].Y = evt.pageY;
2319
-
2320
- this.twoFingerMove(
2321
- this.getMousePosition({
2322
- clientX: this.touches[i].targets[0].X,
2323
- clientY: this.touches[i].targets[0].Y
2324
- }),
2325
- this.getMousePosition({
2326
- clientX: this.touches[i].targets[1].X,
2327
- clientY: this.touches[i].targets[1].Y
2328
- }),
2329
- this.touches[i], evt);
2356
+ this.twoFingerMove(this.touches[i], evt.pointerId, evt);
2357
+
2358
+ touchTargets[j].Xprev = pos[0];
2359
+ touchTargets[j].Yprev = pos[1];
2330
2360
  }
2331
2361
 
2332
- // there is only one pointer in the evt object, there's no point in looking further
2362
+ // There is only one pointer in the evt object, so there's no point in looking further
2333
2363
  break;
2334
2364
  }
2335
2365
  }
2336
2366
  }
2337
2367
  } else {
2338
2368
  if (this._getPointerInputDevice(evt) === 'touch') {
2339
- this._pointerAddBoardTouches(evt);
2369
+ this._pointerStorePosition(evt);
2370
+
2340
2371
  if (this._board_touches.length === 2) {
2341
2372
  evt.touches = this._board_touches;
2342
2373
  this.gestureChangeListener(evt);
@@ -2366,21 +2397,20 @@ define([
2366
2397
  * @returns {Boolean}
2367
2398
  */
2368
2399
  pointerUpListener: function (evt) {
2369
- var i, j, found;
2400
+ var i, j, found, touchTargets;
2370
2401
 
2371
2402
  this.triggerEventHandlers(['touchend', 'up', 'pointerup', 'MSPointerUp'], [evt]);
2372
2403
  this.displayInfobox(false);
2373
2404
 
2374
2405
  if (evt) {
2375
2406
  for (i = 0; i < this.touches.length; i++) {
2376
- for (j = 0; j < this.touches[i].targets.length; j++) {
2377
- if (this.touches[i].targets[j].num === evt.pointerId) {
2378
- this.touches[i].targets.splice(j, 1);
2379
-
2380
- if (this.touches[i].targets.length === 0) {
2407
+ touchTargets = this.touches[i].targets;
2408
+ for (j = 0; j < touchTargets.length; j++) {
2409
+ if (touchTargets[j].num === evt.pointerId) {
2410
+ touchTargets.splice(j, 1);
2411
+ if (touchTargets.length === 0) {
2381
2412
  this.touches.splice(i, 1);
2382
2413
  }
2383
-
2384
2414
  break;
2385
2415
  }
2386
2416
  }
@@ -2408,27 +2438,27 @@ define([
2408
2438
  }
2409
2439
  }
2410
2440
 
2411
- this._pointerRemoveBoardTouches(evt);
2412
-
2413
- // if (this.touches.length === 0) {
2414
- if (this._board_touches.length === 0) {
2441
+ // this._pointerRemoveTouches(evt);
2442
+ // if (this._board_touches.length === 0) {
2415
2443
  if (this.hasPointerUp) {
2416
2444
  if (window.navigator.msPointerEnabled) { // IE10-
2417
- Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2445
+ Env.removeEvent(this.document, 'MSPointerUp', this.pointerUpListener, this);
2418
2446
  } else {
2419
- Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
2447
+ Env.removeEvent(this.document, 'pointerup', this.pointerUpListener, this);
2448
+ Env.removeEvent(this.document, 'pointercancel', this.pointerUpListener, this);
2420
2449
  }
2421
2450
  this.hasPointerUp = false;
2422
2451
  }
2423
2452
 
2424
- this.dehighlightAll();
2425
- this.updateQuality = this.BOARD_QUALITY_HIGH;
2426
- this.mode = this.BOARD_MODE_NONE;
2453
+ // this.dehighlightAll();
2454
+ // this.updateQuality = this.BOARD_QUALITY_HIGH;
2455
+ // this.mode = this.BOARD_MODE_NONE;
2427
2456
 
2428
2457
  this.originMoveEnd();
2429
2458
  this.update();
2430
- }
2431
-
2459
+ // }
2460
+ // After one finger leaves the screen the gesture is stopped.
2461
+ this._pointerClearTouches();
2432
2462
  return true;
2433
2463
  },
2434
2464
 
@@ -2446,7 +2476,7 @@ define([
2446
2476
  eps = this.options.precision.touch,
2447
2477
  obj, found, targets,
2448
2478
  evtTouches = evt[JXG.touchProperty],
2449
- target;
2479
+ target, touchTargets;
2450
2480
 
2451
2481
  if (!this.hasTouchEnd) {
2452
2482
  Env.addEvent(this.document, 'touchend', this.touchEndListener, this);
@@ -2494,19 +2524,19 @@ define([
2494
2524
  }
2495
2525
 
2496
2526
  for (i = 0; i < this.touches.length; i++) {
2497
- for (j = 0; j < this.touches[i].targets.length; j++) {
2498
- this.touches[i].targets[j].num = -1;
2527
+ touchTargets = this.touches[i].targets;
2528
+ for (j = 0; j < touchTargets.length; j++) {
2529
+ touchTargets[j].num = -1;
2499
2530
  eps = this.options.precision.touch;
2500
2531
 
2501
2532
  do {
2502
2533
  for (k = 0; k < evtTouches.length; k++) {
2503
2534
  // find the new targettouches
2504
- if (Math.abs(Math.pow(evtTouches[k].screenX - this.touches[i].targets[j].X, 2) +
2505
- Math.pow(evtTouches[k].screenY - this.touches[i].targets[j].Y, 2)) < eps * eps) {
2506
- this.touches[i].targets[j].num = k;
2507
-
2508
- this.touches[i].targets[j].X = evtTouches[k].screenX;
2509
- this.touches[i].targets[j].Y = evtTouches[k].screenY;
2535
+ if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) +
2536
+ Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2537
+ touchTargets[j].num = k;
2538
+ touchTargets[j].X = evtTouches[k].screenX;
2539
+ touchTargets[j].Y = evtTouches[k].screenY;
2510
2540
  evtTouches[k].jxg_isused = true;
2511
2541
  break;
2512
2542
  }
@@ -2514,13 +2544,13 @@ define([
2514
2544
 
2515
2545
  eps *= 2;
2516
2546
 
2517
- } while (this.touches[i].targets[j].num === -1 &&
2547
+ } while (touchTargets[j].num === -1 &&
2518
2548
  eps < this.options.precision.touchMax);
2519
2549
 
2520
- if (this.touches[i].targets[j].num === -1) {
2550
+ if (touchTargets[j].num === -1) {
2521
2551
  JXG.debug('i couldn\'t find a targettouches for target no ' + j + ' on ' + this.touches[i].obj.name + ' (' + this.touches[i].obj.id + '). Removed the target.');
2522
2552
  JXG.debug('eps = ' + eps + ', touchMax = ' + Options.precision.touchMax);
2523
- this.touches[i].targets.splice(i, 1);
2553
+ touchTargets.splice(i, 1);
2524
2554
  }
2525
2555
 
2526
2556
  }
@@ -2546,20 +2576,22 @@ define([
2546
2576
  elements = this.initMoveObject(pos[0], pos[1], evt, 'touch');
2547
2577
  if (elements.length !== 0) {
2548
2578
  obj = elements[elements.length - 1];
2579
+ target = {num: i,
2580
+ X: evtTouches[i].screenX,
2581
+ Y: evtTouches[i].screenY,
2582
+ Xprev: NaN,
2583
+ Yprev: NaN,
2584
+ Xstart: [],
2585
+ Ystart: [],
2586
+ Zstart: []
2587
+ };
2549
2588
 
2550
2589
  if (Type.isPoint(obj) ||
2551
2590
  obj.elementClass === Const.OBJECT_CLASS_TEXT ||
2552
2591
  obj.type === Const.OBJECT_TYPE_TICKS ||
2553
2592
  obj.type === Const.OBJECT_TYPE_IMAGE) {
2554
- // it's a point, so it's single touch, so we just push it to our touches
2555
- targets = [{num: i,
2556
- X: evtTouches[i].screenX,
2557
- Y: evtTouches[i].screenY,
2558
- Xprev: NaN,
2559
- Yprev: NaN,
2560
- Xstart: [],
2561
- Ystart: [],
2562
- Zstart: [] }];
2593
+ // It's a point, so it's single touch, so we just push it to our touches
2594
+ targets = [obj];
2563
2595
 
2564
2596
  // For the UNDO/REDO of object moves
2565
2597
  this.saveStartPos(obj, targets[0]);
@@ -2579,15 +2611,6 @@ define([
2579
2611
  found = true;
2580
2612
  // only add it, if we don't have two targets in there already
2581
2613
  if (this.touches[j].targets.length === 1) {
2582
- target = {num: i,
2583
- X: evtTouches[i].screenX,
2584
- Y: evtTouches[i].screenY,
2585
- Xprev: NaN,
2586
- Yprev: NaN,
2587
- Xstart: [],
2588
- Ystart: [],
2589
- Zstart: [] };
2590
-
2591
2614
  // For the UNDO/REDO of object moves
2592
2615
  this.saveStartPos(obj, target);
2593
2616
  this.touches[j].targets.push(target);
@@ -2601,14 +2624,7 @@ define([
2601
2624
  // IF there is a second touch targetting this line, we will find it later on, and then add it to
2602
2625
  // the touches control object.
2603
2626
  if (!found) {
2604
- targets = [{num: i,
2605
- X: evtTouches[i].screenX,
2606
- Y: evtTouches[i].screenY,
2607
- Xprev: NaN,
2608
- Yprev: NaN,
2609
- Xstart: [],
2610
- Ystart: [],
2611
- Zstart: [] }];
2627
+ targets = [target];
2612
2628
 
2613
2629
  // For the UNDO/REDO of object moves
2614
2630
  this.saveStartPos(obj, targets[0]);
@@ -2629,12 +2645,10 @@ define([
2629
2645
 
2630
2646
  // Touch events on empty areas of the board are handled here:
2631
2647
  // 1. case: one finger. If allowed, this triggers pan with one finger
2632
- if (this.mode === this.BOARD_MODE_NONE && this.touchOriginMoveStart(evt)) {
2648
+ if (evtTouches.length === 1 && this.mode === this.BOARD_MODE_NONE && this.touchStartMoveOriginOneFinger(evt)) {
2633
2649
  } else if (evtTouches.length === 2 &&
2634
- (this.mode === this.BOARD_MODE_NONE ||
2635
- this.mode === this.BOARD_MODE_MOVE_ORIGIN /*||
2636
- (this.mode === this.BOARD_MODE_DRAG && this.touches.length == 1) */
2637
- )) {
2650
+ (this.mode === this.BOARD_MODE_NONE || this.mode === this.BOARD_MODE_MOVE_ORIGIN)
2651
+ ) {
2638
2652
  // 2. case: two fingers: pinch to zoom or pan with two fingers needed.
2639
2653
  // This happens when the second finger hits the device. First, the
2640
2654
  // "one finger pan mode" has to be cancelled.
@@ -2657,7 +2671,8 @@ define([
2657
2671
  * @returns {Boolean}
2658
2672
  */
2659
2673
  touchMoveListener: function (evt) {
2660
- var i, pos1, pos2, time,
2674
+ var i, pos1, pos2,
2675
+ touchTargets,
2661
2676
  evtTouches = evt[JXG.touchProperty];
2662
2677
 
2663
2678
  if (!this.checkFrameRate(evt)) {
@@ -2694,40 +2709,52 @@ define([
2694
2709
  // Runs over through all elements which are touched
2695
2710
  // by at least one finger.
2696
2711
  for (i = 0; i < this.touches.length; i++) {
2697
- if (this.touches[i].targets.length === 1) {
2712
+ touchTargets = this.touches[i].targets;
2713
+ if (touchTargets.length === 1) {
2714
+
2715
+
2698
2716
  // Touch by one finger: this is possible for all elements that can be dragged
2699
- if (evtTouches[this.touches[i].targets[0].num]) {
2700
- pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
2717
+ if (evtTouches[touchTargets[0].num]) {
2718
+ pos1 = this.getMousePosition(evt, touchTargets[0].num);
2701
2719
  if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2702
2720
  pos1[1] < 0 || pos1[1] > this.canvasHeight) {
2703
2721
  return;
2704
2722
  }
2705
- this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2706
- this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2723
+ touchTargets[0].X = pos1[0];
2724
+ touchTargets[0].Y = pos1[1];
2707
2725
  this.moveObject(pos1[0], pos1[1], this.touches[i], evt, 'touch');
2708
2726
  }
2709
2727
 
2710
- } else if (this.touches[i].targets.length === 2 &&
2711
- this.touches[i].targets[0].num > -1 &&
2712
- this.touches[i].targets[1].num > -1) {
2728
+ } else if (touchTargets.length === 2 &&
2729
+ touchTargets[0].num > -1 &&
2730
+ touchTargets[1].num > -1) {
2731
+
2713
2732
  // Touch by two fingers: moving lines, ...
2714
- if (evtTouches[this.touches[i].targets[0].num] &&
2715
- evtTouches[this.touches[i].targets[1].num]) {
2733
+ if (evtTouches[touchTargets[0].num] &&
2734
+ evtTouches[touchTargets[1].num]) {
2716
2735
 
2717
2736
  // Get coordinates of the two touches
2718
- pos1 = this.getMousePosition(evt, this.touches[i].targets[0].num);
2719
- pos2 = this.getMousePosition(evt, this.touches[i].targets[1].num);
2737
+ pos1 = this.getMousePosition(evt, touchTargets[0].num);
2738
+ pos2 = this.getMousePosition(evt, touchTargets[1].num);
2720
2739
  if (pos1[0] < 0 || pos1[0] > this.canvasWidth ||
2721
2740
  pos1[1] < 0 || pos1[1] > this.canvasHeight ||
2722
2741
  pos2[0] < 0 || pos2[0] > this.canvasWidth ||
2723
2742
  pos2[1] < 0 || pos2[1] > this.canvasHeight) {
2724
2743
  return;
2725
2744
  }
2726
- this.touches[i].targets[0].X = evtTouches[this.touches[i].targets[0].num].screenX;
2727
- this.touches[i].targets[0].Y = evtTouches[this.touches[i].targets[0].num].screenY;
2728
- this.touches[i].targets[1].X = evtTouches[this.touches[i].targets[1].num].screenX;
2729
- this.touches[i].targets[1].Y = evtTouches[this.touches[i].targets[1].num].screenY;
2730
- this.twoFingerMove(pos1, pos2, this.touches[i], evt);
2745
+
2746
+ touchTargets[0].X = pos1[0];
2747
+ touchTargets[0].Y = pos1[1];
2748
+ touchTargets[1].X = pos2[0];
2749
+ touchTargets[1].Y = pos2[1];
2750
+
2751
+ this.twoFingerMove(this.touches[i], touchTargets[0].num, evt);
2752
+ this.twoFingerMove(this.touches[i], touchTargets[1].num);
2753
+
2754
+ touchTargets[0].Xprev = pos1[0];
2755
+ touchTargets[0].Yprev = pos1[1];
2756
+ touchTargets[1].Xprev = pos2[0];
2757
+ touchTargets[1].Yprev = pos2[1];
2731
2758
  }
2732
2759
  }
2733
2760
  }
@@ -2762,7 +2789,8 @@ define([
2762
2789
  var i, j, k,
2763
2790
  eps = this.options.precision.touch,
2764
2791
  tmpTouches = [], found, foundNumber,
2765
- evtTouches = evt && evt[JXG.touchProperty];
2792
+ evtTouches = evt && evt[JXG.touchProperty],
2793
+ touchTargets;
2766
2794
 
2767
2795
  this.triggerEventHandlers(['touchend', 'up'], [evt]);
2768
2796
  this.displayInfobox(false);
@@ -2798,15 +2826,16 @@ define([
2798
2826
  // could all targets of the current this.touches.obj be assigned to targettouches?
2799
2827
  found = false;
2800
2828
  foundNumber = 0;
2829
+ touchTargets = tmpTouches[i].targets;
2801
2830
 
2802
- for (j = 0; j < tmpTouches[i].targets.length; j++) {
2803
- tmpTouches[i].targets[j].found = false;
2831
+ for (j = 0; j < touchTargets.length; j++) {
2832
+ touchTargets[j].found = false;
2804
2833
  for (k = 0; k < evtTouches.length; k++) {
2805
- if (Math.abs(Math.pow(evtTouches[k].screenX - tmpTouches[i].targets[j].X, 2) + Math.pow(evtTouches[k].screenY - tmpTouches[i].targets[j].Y, 2)) < eps * eps) {
2806
- tmpTouches[i].targets[j].found = true;
2807
- tmpTouches[i].targets[j].num = k;
2808
- tmpTouches[i].targets[j].X = evtTouches[k].screenX;
2809
- tmpTouches[i].targets[j].Y = evtTouches[k].screenY;
2834
+ if (Math.abs(Math.pow(evtTouches[k].screenX - touchTargets[j].X, 2) + Math.pow(evtTouches[k].screenY - touchTargets[j].Y, 2)) < eps * eps) {
2835
+ touchTargets[j].found = true;
2836
+ touchTargets[j].num = k;
2837
+ touchTargets[j].X = evtTouches[k].screenX;
2838
+ touchTargets[j].Y = evtTouches[k].screenY;
2810
2839
  foundNumber += 1;
2811
2840
  break;
2812
2841
  }
@@ -2814,9 +2843,9 @@ define([
2814
2843
  }
2815
2844
 
2816
2845
  if (Type.isPoint(tmpTouches[i].obj)) {
2817
- found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found);
2846
+ found = (touchTargets[0] && touchTargets[0].found);
2818
2847
  } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_LINE) {
2819
- found = (tmpTouches[i].targets[0] && tmpTouches[i].targets[0].found) || (tmpTouches[i].targets[1] && tmpTouches[i].targets[1].found);
2848
+ found = (touchTargets[0] && touchTargets[0].found) || (touchTargets[1] && touchTargets[1].found);
2820
2849
  } else if (tmpTouches[i].obj.elementClass === Const.OBJECT_CLASS_CIRCLE) {
2821
2850
  found = foundNumber === 1 || foundNumber === 3;
2822
2851
  }
@@ -2828,17 +2857,17 @@ define([
2828
2857
  targets: []
2829
2858
  });
2830
2859
 
2831
- for (j = 0; j < tmpTouches[i].targets.length; j++) {
2832
- if (tmpTouches[i].targets[j].found) {
2860
+ for (j = 0; j < touchTargets.length; j++) {
2861
+ if (touchTargets[j].found) {
2833
2862
  this.touches[this.touches.length - 1].targets.push({
2834
- num: tmpTouches[i].targets[j].num,
2835
- X: tmpTouches[i].targets[j].screenX,
2836
- Y: tmpTouches[i].targets[j].screenY,
2863
+ num: touchTargets[j].num,
2864
+ X: touchTargets[j].screenX,
2865
+ Y: touchTargets[j].screenY,
2837
2866
  Xprev: NaN,
2838
2867
  Yprev: NaN,
2839
- Xstart: tmpTouches[i].targets[j].Xstart,
2840
- Ystart: tmpTouches[i].targets[j].Ystart,
2841
- Zstart: tmpTouches[i].targets[j].Zstart
2868
+ Xstart: touchTargets[j].Xstart,
2869
+ Ystart: touchTargets[j].Ystart,
2870
+ Zstart: touchTargets[j].Zstart
2842
2871
  });
2843
2872
  }
2844
2873
  }
@@ -2928,6 +2957,7 @@ define([
2928
2957
  this.mode = this.BOARD_MODE_NONE;
2929
2958
  result = true;
2930
2959
  } else {
2960
+ /** @ignore */
2931
2961
  this.mouse = {
2932
2962
  obj: null,
2933
2963
  targets: [{
@@ -3052,6 +3082,7 @@ define([
3052
3082
  }
3053
3083
 
3054
3084
  // release dragged mouse object
3085
+ /** @ignore */
3055
3086
  this.mouse = null;
3056
3087
  },
3057
3088
 
@@ -3080,6 +3111,7 @@ define([
3080
3111
  evt.preventDefault();
3081
3112
  return false;
3082
3113
  },
3114
+
3083
3115
  /**
3084
3116
  * Allow moving of JSXGraph elements with arrow keys
3085
3117
  * and zooming of the construction with + / -.
@@ -3096,7 +3128,11 @@ define([
3096
3128
  */
3097
3129
  keyDownListener: function (evt) {
3098
3130
  var id_node = evt.target.id,
3099
- id, el,
3131
+ id, el, res,
3132
+ sX = 0,
3133
+ sY = 0,
3134
+ // dx, dy are provided in screen units and
3135
+ // are converted to user coordinates
3100
3136
  dx = Type.evaluate(this.attr.keyboard.dx) / this.unitX,
3101
3137
  dy = Type.evaluate(this.attr.keyboard.dy) / this.unitY,
3102
3138
  doZoom = false,
@@ -3130,6 +3166,40 @@ define([
3130
3166
  this.clickRightArrow();
3131
3167
  }
3132
3168
  } else {
3169
+ // Adapt dx, dy to snapToGrid and attractToGrid
3170
+ // snapToGrid has priority.
3171
+ if (Type.exists(el.visProp)) {
3172
+ if (Type.exists(el.visProp.snaptogrid) &&
3173
+ el.visProp.snaptogrid &&
3174
+ Type.evaluate(el.visProp.snapsizex) &&
3175
+ Type.evaluate(el.visProp.snapsizey)) {
3176
+
3177
+ // Adapt dx, dy such that snapToGrid is possible
3178
+ res = el.getSnapSizes();
3179
+ sX = res[0];
3180
+ sY = res[1];
3181
+ dx = Math.max(sX, dx);
3182
+ dy = Math.max(sY, dy);
3183
+
3184
+ } else if (Type.exists(el.visProp.attracttogrid) &&
3185
+ el.visProp.attracttogrid &&
3186
+ Type.evaluate(el.visProp.attractordistance) &&
3187
+ Type.evaluate(el.visProp.attractorunit)) {
3188
+
3189
+ // Adapt dx, dy such that attractToGrid is possible
3190
+ sX = 1.1 * Type.evaluate(el.visProp.attractordistance);
3191
+ sY = sX;
3192
+
3193
+ if (Type.evaluate(el.visProp.attractorunit) === 'screen') {
3194
+ sX /= this.unitX;
3195
+ sY /= this.unitX;
3196
+ }
3197
+ dx = Math.max(sX, dx);
3198
+ dy = Math.max(sY, dy);
3199
+ }
3200
+
3201
+ }
3202
+
3133
3203
  if (evt.keyCode === 38) { // up
3134
3204
  dir = [0, dy];
3135
3205
  } else if (evt.keyCode === 40) { // down
@@ -3144,7 +3214,7 @@ define([
3144
3214
  this.zoomIn();
3145
3215
  } else if (doZoom && evt.key === '-') { // -
3146
3216
  this.zoomOut();
3147
- } else if (doZoom && evt.key === 'o') { // o
3217
+ } else if (doZoom && evt.key === 'o') { // o
3148
3218
  this.zoom100();
3149
3219
  }
3150
3220
  if (dir && el.isDraggable &&
@@ -3163,6 +3233,10 @@ define([
3163
3233
  // For coordsElement setPosition has to call setPositionDirectly.
3164
3234
  // Otherwise the position is set by a translation.
3165
3235
  el.setPosition(JXG.COORDS_BY_USER, dir);
3236
+ if (Type.exists(el.coords)) {
3237
+ this.updateInfobox(el);
3238
+ }
3239
+ this.triggerEventHandlers(['hit'], [evt, el]);
3166
3240
  }
3167
3241
  }
3168
3242
 
@@ -3194,6 +3268,10 @@ define([
3194
3268
  if (Type.exists(el.highlight)) {
3195
3269
  el.highlight(true);
3196
3270
  }
3271
+ if (Type.exists(el.coords)) {
3272
+ this.updateInfobox(el);
3273
+ }
3274
+ this.triggerEventHandlers(['hit'], [evt, el]);
3197
3275
  },
3198
3276
 
3199
3277
  /**
@@ -3206,7 +3284,7 @@ define([
3206
3284
  *
3207
3285
  * @param {Event} evt The browser's event object
3208
3286
  */
3209
- keyFocusOutListener: function (evt) {
3287
+ keyFocusOutListener: function (evt) {
3210
3288
  if (!this.attr.keyboard.enabled) {
3211
3289
  return false;
3212
3290
  }
@@ -3216,6 +3294,7 @@ define([
3216
3294
  // id = id_node.replace(this.containerObj.id + '_', '');
3217
3295
  // el = this.select(id);
3218
3296
  this.dehighlightAll();
3297
+ this.displayInfobox(false);
3219
3298
  },
3220
3299
 
3221
3300
  /**
@@ -3232,34 +3311,45 @@ define([
3232
3311
  *
3233
3312
  */
3234
3313
  updateContainerDims: function() {
3235
- var theWidth, theHeight;
3314
+ var w, h,
3315
+ bb, css;
3236
3316
 
3237
- theWidth = this.containerObj.getBoundingClientRect().width;
3238
- theHeight = this.containerObj.getBoundingClientRect().height;
3239
- if (theWidth === 0 || theHeight === 0) {
3240
- // The div is invisible - do nothing
3317
+ // Get size of the board's container div
3318
+ bb = this.containerObj.getBoundingClientRect();
3319
+ w = bb.width;
3320
+ h = bb.height;
3321
+
3322
+ // Subtract the border size
3323
+ if (window && window.getComputedStyle) {
3324
+ css = window.getComputedStyle(this.containerObj, null);
3325
+ w -= parseFloat(css.getPropertyValue('border-left-width')) + parseFloat(css.getPropertyValue('border-right-width'));
3326
+ h -= parseFloat(css.getPropertyValue('border-top-width')) + parseFloat(css.getPropertyValue('border-bottom-width'));
3327
+ }
3328
+
3329
+ // If div is invisible - do nothing
3330
+ if (w <= 0 || h <= 0) {
3241
3331
  return;
3242
3332
  }
3243
3333
 
3244
3334
  // If bounding box is not yet initialized, do it now.
3245
3335
  if (isNaN(this.getBoundingBox()[0])) {
3246
- this.setBoundingBox(this.attr.boundingbox);
3336
+ this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'keep');
3247
3337
  }
3248
3338
 
3249
- // We do nothing if in case the dimension did not change since being visible
3339
+ // Do nothing if the dimension did not change since being visible
3250
3340
  // the last time. Note that if the div had display:none in the mean time,
3251
- // we did not store this._prevTheWidth/Height.
3252
- if (Type.exists(this._prevTheWidth) && Type.exists(this._prevTheHeight) &&
3253
- this._prevTheWidth === theWidth && this._prevTheHeight === theHeight) {
3341
+ // we did not store this._prevDim.
3342
+ if (Type.exists(this._prevDim) &&
3343
+ this._prevDim.w === w && this._prevDim.h === h) {
3254
3344
  return;
3255
3345
  }
3256
- // theWidth and theHeight are the outer dimensions and therefore
3257
- // might be too large. This does no harm, because the result is only that the
3258
- // SVGRoot might be larger than the visible div and there
3259
- // is overflow:hidden.
3260
- this.resizeContainer(theWidth, theHeight, true);
3261
- this._prevTheWidth = theWidth;
3262
- this._prevTheHeight = theHeight;
3346
+
3347
+ // Set the size of the SVG or canvas element
3348
+ this.resizeContainer(w, h, true);
3349
+ this._prevDim = {
3350
+ w: w,
3351
+ h: h
3352
+ };
3263
3353
  },
3264
3354
 
3265
3355
  /**
@@ -3288,8 +3378,13 @@ define([
3288
3378
  if (!that._isResizing) {
3289
3379
  that._isResizing = true;
3290
3380
  window.setTimeout(function() {
3291
- that.updateContainerDims();
3292
- that._isResizing = false;
3381
+ try {
3382
+ that.updateContainerDims();
3383
+ } catch (err) {
3384
+ that.stopResizeObserver();
3385
+ } finally {
3386
+ that._isResizing = false;
3387
+ }
3293
3388
  }, that.attr.resize.throttle);
3294
3389
  }
3295
3390
  });
@@ -3378,13 +3473,12 @@ define([
3378
3473
  this.intersectionObserver = new IntersectionObserver(function(entries) {
3379
3474
  // If bounding box is not yet initialized, do it now.
3380
3475
  if (isNaN(that.getBoundingBox()[0])) {
3381
- that.setBoundingBox(that.attr.boundingbox);
3382
3476
  that.updateContainerDims();
3383
3477
  }
3384
3478
  }, options);
3385
3479
  this.intersectionObserver.observe(that.containerObj);
3386
3480
  } catch (err) {
3387
- console.log('Info: IntersectionObserver not available in this browser');
3481
+ console.log('JSXGraph: IntersectionObserver not available in this browser.');
3388
3482
  }
3389
3483
  },
3390
3484
 
@@ -3419,7 +3513,7 @@ define([
3419
3513
  * Infobox close to points in which the points' coordinates are displayed.
3420
3514
  * This is simply a JXG.Text element. Access through board.infobox.
3421
3515
  * Uses CSS class .JXGinfobox.
3422
- * @type {JXG.Text}
3516
+ * @type JXG.Text
3423
3517
  *
3424
3518
  */
3425
3519
  this.infobox = this.create('text', [0, 0, '0,0'], attr);
@@ -3611,11 +3705,11 @@ define([
3611
3705
  * // Shorter version:
3612
3706
  * //somePoint = board.create('point', a, {name:'SomePoint',size:4});
3613
3707
  * });
3614
- *
3708
+ *
3615
3709
  * })();
3616
3710
  *
3617
3711
  * </script><pre>
3618
- *
3712
+ *
3619
3713
  * @see JXG.Board#getScrCoordsOfMouse
3620
3714
  * @see JXG.Board#getAllUnderMouse
3621
3715
  */
@@ -3737,7 +3831,7 @@ define([
3737
3831
  addConditions: function (str) {
3738
3832
  var term, m, left, right, name, el, property,
3739
3833
  functions = [],
3740
- plaintext = 'var el, x, y, c, rgbo;\n',
3834
+ // plaintext = 'var el, x, y, c, rgbo;\n',
3741
3835
  i = str.indexOf('<data>'),
3742
3836
  j = str.indexOf('<' + '/data>'),
3743
3837
 
@@ -3821,7 +3915,7 @@ define([
3821
3915
  if (!Type.exists(this.elementsByName[name])) {
3822
3916
  JXG.debug("debug conditions: |" + name + "| undefined");
3823
3917
  } else {
3824
- plaintext += "el = this.objects[\"" + el.id + "\"];\n";
3918
+ // plaintext += "el = this.objects[\"" + el.id + "\"];\n";
3825
3919
 
3826
3920
  switch (property) {
3827
3921
  case 'x':
@@ -3948,9 +4042,7 @@ define([
3948
4042
  tr = (bb[1] - y) / (bb[1] - bb[3]);
3949
4043
  }
3950
4044
 
3951
- this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
3952
- this.zoomX *= zX;
3953
- this.zoomY *= zY;
4045
+ this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
3954
4046
  return this.applyZoom();
3955
4047
  },
3956
4048
 
@@ -3981,25 +4073,32 @@ define([
3981
4073
  tr = (bb[1] - y) / (bb[1] - bb[3]);
3982
4074
  }
3983
4075
 
3984
- this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], false);
3985
- this.zoomX /= zX;
3986
- this.zoomY /= zY;
4076
+ this.setBoundingBox([bb[0] + dX * lr, bb[1] - dY * tr, bb[2] - dX * (1 - lr), bb[3] + dY * (1 - tr)], this.keepaspectratio, 'update');
3987
4077
 
3988
4078
  return this.applyZoom();
3989
4079
  },
3990
4080
 
3991
4081
  /**
3992
- * Resets zoom factor to 100%.
4082
+ * Reset the zoom level to the original zoom level from initBoard();
4083
+ * Additionally, if the board as been initialized with a boundingBox (which is the default),
4084
+ * restore the viewport to the original viewport during initialization. Otherwise,
4085
+ * (i.e. if the board as been initialized with unitX/Y and originX/Y),
4086
+ * just set the zoom level to 100%.
4087
+ *
3993
4088
  * @returns {JXG.Board} Reference to the board
3994
4089
  */
3995
4090
  zoom100: function () {
3996
- var bb = this.getBoundingBox(),
3997
- dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5,
3998
- dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4091
+ var bb, dX, dY;
3999
4092
 
4000
- this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], false);
4001
- this.zoomX = 1.0;
4002
- this.zoomY = 1.0;
4093
+ if (Type.exists(this.attr.boundingbox)) {
4094
+ this.setBoundingBox(this.attr.boundingbox, this.keepaspectratio, 'reset');
4095
+ } else {
4096
+ // Board has been set up with unitX/Y and originX/Y
4097
+ bb = this.getBoundingBox();
4098
+ dX = (bb[2] - bb[0]) * (1.0 - this.zoomX) * 0.5;
4099
+ dY = (bb[1] - bb[3]) * (1.0 - this.zoomY) * 0.5;
4100
+ this.setBoundingBox([bb[0] + dX, bb[1] - dY, bb[2] - dX, bb[3] + dY], this.keepaspectratio, 'reset');
4101
+ }
4003
4102
  return this.applyZoom();
4004
4103
  },
4005
4104
 
@@ -4036,10 +4135,7 @@ define([
4036
4135
  borderX = border / this.unitX;
4037
4136
  borderY = border / this.unitY;
4038
4137
 
4039
- this.zoomX = 1.0;
4040
- this.zoomY = 1.0;
4041
-
4042
- this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], true);
4138
+ this.setBoundingBox([minX - borderX, maxY + borderY, maxX + borderX, minY - borderY], this.keepaspectratio, 'update');
4043
4139
 
4044
4140
  return this.applyZoom();
4045
4141
  },
@@ -4072,53 +4168,12 @@ define([
4072
4168
  }
4073
4169
 
4074
4170
  if (Type.isArray(newBBox)) {
4075
- this.zoomX = 1.0;
4076
- this.zoomY = 1.0;
4077
4171
  cx = 0.5 * (newBBox[0] + newBBox[2]);
4078
4172
  cy = 0.5 * (newBBox[1] + newBBox[3]);
4079
4173
  dx = 1.5 * (newBBox[2] - newBBox[0]) * 0.5;
4080
4174
  dy = 1.5 * (newBBox[1] - newBBox[3]) * 0.5;
4081
4175
  d = Math.max(dx, dy);
4082
- this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], true);
4083
- }
4084
-
4085
- return this;
4086
- },
4087
-
4088
- zoomElementsOld: function (elements) {
4089
- var i, j, e, box,
4090
- newBBox = [0, 0, 0, 0],
4091
- dir = [1, -1, -1, 1];
4092
-
4093
- if (!Type.isArray(elements) || elements.length === 0) {
4094
- return this;
4095
- }
4096
-
4097
- for (i = 0; i < elements.length; i++) {
4098
- e = this.select(elements[i]);
4099
-
4100
- box = e.bounds();
4101
- if (Type.isArray(box)) {
4102
- if (Type.isArray(newBBox)) {
4103
- for (j = 0; j < 4; j++) {
4104
- if (dir[j] * box[j] < dir[j] * newBBox[j]) {
4105
- newBBox[j] = box[j];
4106
- }
4107
- }
4108
- } else {
4109
- newBBox = box;
4110
- }
4111
- }
4112
- }
4113
-
4114
- if (Type.isArray(newBBox)) {
4115
- for (j = 0; j < 4; j++) {
4116
- newBBox[j] -= dir[j];
4117
- }
4118
-
4119
- this.zoomX = 1.0;
4120
- this.zoomY = 1.0;
4121
- this.setBoundingBox(newBBox, true);
4176
+ this.setBoundingBox([cx - d, cy + d, cx + d, cy - d], this.keepaspectratio, 'update');
4122
4177
  }
4123
4178
 
4124
4179
  return this;
@@ -4336,12 +4391,37 @@ define([
4336
4391
  */
4337
4392
  resizeContainer: function (canvasWidth, canvasHeight, dontset, dontSetBoundingBox) {
4338
4393
  var box;
4394
+ // w, h, cx, cy;
4395
+ // box_act,
4396
+ // shift_x = 0,
4397
+ // shift_y = 0;
4339
4398
 
4340
4399
  if (!dontSetBoundingBox) {
4341
- box = this.getBoundingBox();
4400
+ // box_act = this.getBoundingBox(); // This is the actual bounding box.
4401
+ box = this.getBoundingBox(); // This is the actual bounding box.
4342
4402
  }
4343
- this.canvasWidth = parseInt(canvasWidth, 10);
4344
- this.canvasHeight = parseInt(canvasHeight, 10);
4403
+
4404
+ this.canvasWidth = parseFloat(canvasWidth);
4405
+ this.canvasHeight = parseFloat(canvasHeight);
4406
+
4407
+ // if (!dontSetBoundingBox) {
4408
+ // box = this.attr.boundingbox; // This is the intended bounding box.
4409
+
4410
+ // // The shift values compensate the follow-up correction
4411
+ // // in setBoundingBox in case of "this.keepaspectratio==true"
4412
+ // // Otherwise, shift_x and shift_y will be zero.
4413
+ // // Obsolet since setBoundingBox centers in case of "this.keepaspectratio==true".
4414
+ // // shift_x = box_act[0] - box[0] / this.zoomX;
4415
+ // // shift_y = box_act[1] - box[1] / this.zoomY;
4416
+
4417
+ // cx = (box[2] + box[0]) * 0.5; // + shift_x;
4418
+ // cy = (box[3] + box[1]) * 0.5; // + shift_y;
4419
+
4420
+ // w = (box[2] - box[0]) * 0.5 / this.zoomX;
4421
+ // h = (box[1] - box[3]) * 0.5 / this.zoomY;
4422
+
4423
+ // box = [cx - w, cy + h, cx + w, cy - h];
4424
+ // }
4345
4425
 
4346
4426
  if (!dontset) {
4347
4427
  this.containerObj.style.width = (this.canvasWidth) + 'px';
@@ -4350,7 +4430,7 @@ define([
4350
4430
  this.renderer.resize(this.canvasWidth, this.canvasHeight);
4351
4431
 
4352
4432
  if (!dontSetBoundingBox) {
4353
- this.setBoundingBox(box, this.keepaspectratio);
4433
+ this.setBoundingBox(box, this.keepaspectratio, 'keep');
4354
4434
  }
4355
4435
 
4356
4436
  return this;
@@ -4457,13 +4537,13 @@ define([
4457
4537
 
4458
4538
  for (el = 0; el < this.objectsList.length; el++) {
4459
4539
  pEl = this.objectsList[el];
4460
- if (this.needsFullUpdate && pEl.elementClass == Const.OBJECT_CLASS_TEXT) {
4540
+ if (this.needsFullUpdate && pEl.elementClass === Const.OBJECT_CLASS_TEXT) {
4461
4541
  pEl.updateSize();
4462
4542
  }
4463
4543
 
4464
4544
  // For updates of an element we distinguish if the dragged element is updated or
4465
4545
  // other elements are updated.
4466
- // The difference lies in the treatment of gliders.
4546
+ // The difference lies in the treatment of gliders and points based on transformations.
4467
4547
  pEl.update(!Type.exists(drag) || pEl.id !== drag.id)
4468
4548
  .updateVisibility();
4469
4549
  }
@@ -4837,15 +4917,21 @@ define([
4837
4917
  * @param {Array} bbox New bounding box [x1,y1,x2,y2]
4838
4918
  * @param {Boolean} [keepaspectratio=false] If set to true, the aspect ratio will be 1:1, but
4839
4919
  * the resulting viewport may be larger.
4920
+ * @param {String} [setZoom='reset'] Reset, keep or update the zoom level of the board. 'reset'
4921
+ * sets {@link JXG.Board#zoomX} and {@link JXG.Board#zoomY} to the start values (or 1.0).
4922
+ * 'update' adapts these values accoring to the new bounding box and 'keep' does nothing.
4840
4923
  * @returns {JXG.Board} Reference to the board
4841
4924
  */
4842
- setBoundingBox: function (bbox, keepaspectratio) {
4843
- var h, w,
4925
+ setBoundingBox: function (bbox, keepaspectratio, setZoom) {
4926
+ var h, w, ux, uy,
4927
+ offX = 0,
4928
+ offY = 0,
4844
4929
  dim = Env.getDimensions(this.container, this.document);
4845
4930
 
4846
4931
  if (!Type.isArray(bbox)) {
4847
4932
  return this;
4848
4933
  }
4934
+
4849
4935
  if (bbox[0] < this.maxboundingbox[0] ||
4850
4936
  bbox[1] > this.maxboundingbox[1] ||
4851
4937
  bbox[2] > this.maxboundingbox[2] ||
@@ -4853,20 +4939,28 @@ define([
4853
4939
  return this;
4854
4940
  }
4855
4941
 
4856
- this.plainBB = bbox;
4942
+ if (!Type.exists(setZoom)) {
4943
+ setZoom = 'reset';
4944
+ }
4945
+
4946
+ ux = this.unitX;
4947
+ uy = this.unitY;
4857
4948
 
4858
4949
  this.canvasWidth = parseInt(dim.width, 10);
4859
4950
  this.canvasHeight = parseInt(dim.height, 10);
4860
4951
  w = this.canvasWidth;
4861
4952
  h = this.canvasHeight;
4862
-
4863
4953
  if (keepaspectratio) {
4864
4954
  this.unitX = w / (bbox[2] - bbox[0]);
4865
4955
  this.unitY = h / (bbox[1] - bbox[3]);
4866
4956
  if (Math.abs(this.unitX) < Math.abs(this.unitY)) {
4867
4957
  this.unitY = Math.abs(this.unitX) * this.unitY / Math.abs(this.unitY);
4958
+ // Add the additional units in equal portions above and below
4959
+ offY = (h / this.unitY - (bbox[1] - bbox[3])) * 0.5;
4868
4960
  } else {
4869
4961
  this.unitX = Math.abs(this.unitY) * this.unitX / Math.abs(this.unitX);
4962
+ // Add the additional units in equal portions left and right
4963
+ offX = (w / this.unitX - (bbox[2] - bbox[0])) * 0.5;
4870
4964
  }
4871
4965
  this.keepaspectratio = true;
4872
4966
  } else {
@@ -4875,7 +4969,15 @@ define([
4875
4969
  this.keepaspectratio = false;
4876
4970
  }
4877
4971
 
4878
- this.moveOrigin(-this.unitX * bbox[0], this.unitY * bbox[1]);
4972
+ this.moveOrigin(-this.unitX * (bbox[0] - offX), this.unitY * (bbox[1] + offY));
4973
+
4974
+ if (setZoom === 'update') {
4975
+ this.zoomX *= this.unitX / ux;
4976
+ this.zoomY *= this.unitY / uy;
4977
+ } else if (setZoom === 'reset') {
4978
+ this.zoomX = Type.exists(this.attr.zoomx) ? this.attr.zoomx : 1.0;
4979
+ this.zoomY = Type.exists(this.attr.zoomy) ? this.attr.zoomy : 1.0;
4980
+ }
4879
4981
 
4880
4982
  return this;
4881
4983
  },
@@ -5200,7 +5302,7 @@ define([
5200
5302
  for (i = 0; i < l; i++) {
5201
5303
  olist[flist[i].id] = flist[i];
5202
5304
  }
5203
- s = new EComposition(olist);
5305
+ s = new Composition(olist);
5204
5306
  // it's an element which has been deleted (and still hangs around, e.g. in an attractor list
5205
5307
  } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) {
5206
5308
  s = null;
@@ -5547,7 +5649,7 @@ define([
5547
5649
  * @name JXG.Board#move
5548
5650
  * @param {Event} e The browser's event object.
5549
5651
  * @param {Number} mode The mode the board currently is in
5550
- * @see {JXG.Board#mode}
5652
+ * @see JXG.Board#mode
5551
5653
  */
5552
5654
  __evt__move: function (e, mode) { },
5553
5655
 
@@ -5557,7 +5659,7 @@ define([
5557
5659
  * @name JXG.Board#mousemove
5558
5660
  * @param {Event} e The browser's event object.
5559
5661
  * @param {Number} mode The mode the board currently is in
5560
- * @see {JXG.Board#mode}
5662
+ * @see JXG.Board#mode
5561
5663
  */
5562
5664
  __evt__mousemove: function (e, mode) { },
5563
5665
 
@@ -5567,7 +5669,7 @@ define([
5567
5669
  * @name JXG.Board#penmove
5568
5670
  * @param {Event} e The browser's event object.
5569
5671
  * @param {Number} mode The mode the board currently is in
5570
- * @see {JXG.Board#mode}
5672
+ * @see JXG.Board#mode
5571
5673
  */
5572
5674
  __evt__penmove: function (e, mode) { },
5573
5675
 
@@ -5578,7 +5680,7 @@ define([
5578
5680
  * @name JXG.Board#pointermove
5579
5681
  * @param {Event} e The browser's event object.
5580
5682
  * @param {Number} mode The mode the board currently is in
5581
- * @see {JXG.Board#mode}
5683
+ * @see JXG.Board#mode
5582
5684
  */
5583
5685
  __evt__pointermove: function (e, mode) { },
5584
5686
 
@@ -5588,7 +5690,7 @@ define([
5588
5690
  * @name JXG.Board#touchmove
5589
5691
  * @param {Event} e The browser's event object.
5590
5692
  * @param {Number} mode The mode the board currently is in
5591
- * @see {JXG.Board#mode}
5693
+ * @see JXG.Board#mode
5592
5694
  */
5593
5695
  __evt__touchmove: function (e, mode) { },
5594
5696
 
@@ -5758,6 +5860,12 @@ define([
5758
5860
  * <p>
5759
5861
  * The wrapping div has the CSS class 'jxgbox_wrap_private' which is
5760
5862
  * defined in the file 'jsxgraph.css'
5863
+ * <p>
5864
+ * This feature is not available on iPhones (as of December 2021).
5865
+ *
5866
+ * @param {String} id (Optional) id of the div element which is brought to fullscreen.
5867
+ * If not provided, this defaults to the JSXGraph div. However, it may be necessary for the aspect ratio trick
5868
+ * which using padding-bottom/top and an out div element. Then, the id of the outer div has to be supplied.
5761
5869
  *
5762
5870
  * @return {JXG.Board} Reference to the board
5763
5871
  *
@@ -5779,29 +5887,76 @@ define([
5779
5887
  * var p = board.create('point', [0, 1]);
5780
5888
  * board_d5bab8b6 = board;
5781
5889
  * })();
5782
- * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
5783
- *
5784
5890
  * </script>
5891
+ * <button onClick="board_d5bab8b6.toFullscreen()">Fullscreen</button>
5785
5892
  * <pre>
5786
5893
  *
5894
+ * @example
5895
+ * &lt;div id='outer' style='max-width: 500px; margin: 0 auto;'&gt;
5896
+ * &lt;div id='jxgbox' class='jxgbox' style='height: 0; padding-bottom: 100%'&gt;&lt;/div&gt;
5897
+ * &lt;/div&gt;
5898
+ * &lt;button onClick="board.toFullscreen('outer')"&gt;Fullscreen&lt;/button&gt;
5899
+ *
5900
+ * &lt;script language="Javascript" type='text/javascript'&gt;
5901
+ * var board = JXG.JSXGraph.initBoard('jxgbox', {
5902
+ * axis:true,
5903
+ * boundingbox:[-5,5,5,-5],
5904
+ * fullscreen: { id: 'outer' },
5905
+ * showFullscreen: true
5906
+ * });
5907
+ * var p = board.create('point', [-2, 3], {});
5908
+ * &lt;/script&gt;
5909
+ *
5910
+ * </pre><div id="JXG7103f6b_outer" style='max-width: 500px; margin: 0 auto;'>
5911
+ * <div id="JXG7103f6be-6993-4ff8-8133-c78e50a8afac" class="jxgbox" style="height: 0; padding-bottom: 100%;"></div>
5912
+ * </div>
5913
+ * <button onClick="board_JXG7103f6be.toFullscreen('JXG7103f6b_outer')">Fullscreen</button>
5914
+ * <script type="text/javascript">
5915
+ * var board_JXG7103f6be;
5916
+ * (function() {
5917
+ * var board = JXG.JSXGraph.initBoard('JXG7103f6be-6993-4ff8-8133-c78e50a8afac',
5918
+ * {boundingbox: [-8, 8, 8,-8], axis: true, fullscreen: { id: 'JXG7103f6b_outer' }, showFullscreen: true,
5919
+ * showcopyright: false, shownavigation: false});
5920
+ * var p = board.create('point', [-2, 3], {});
5921
+ * board_JXG7103f6be = board;
5922
+ * })();
5923
+ *
5924
+ * </script><pre>
5925
+ *
5926
+ *
5787
5927
  */
5788
- toFullscreen: function() {
5789
- var id = this.container,
5790
- wrap_id = 'fullscreenwrap_' + id,
5791
- wrapper = document.createElement('div'),
5792
- el;
5928
+ toFullscreen: function (id) {
5929
+ var wrap_id, wrap_node, inner_node;
5930
+
5931
+ id = id || this.container;
5932
+ this._fullscreen_inner_id = id;
5933
+ inner_node = document.getElementById(id);
5934
+ wrap_id = 'fullscreenwrap_' + id;
5793
5935
 
5794
- // If necessary, wrap a div around the JSXGraph div.
5795
- if (!this.document.getElementById(wrap_id)) {
5796
- wrapper.classList.add('JXG_wrap_private');
5797
- wrapper.setAttribute('id', wrap_id);
5798
- el = this.containerObj;
5799
- el.parentNode.insertBefore(wrapper, el);
5800
- wrapper.appendChild(el);
5936
+ // Wrap a div around the JSXGraph div.
5937
+ if (this.document.getElementById(wrap_id)) {
5938
+ wrap_node = this.document.getElementById(wrap_id);
5939
+ } else {
5940
+ wrap_node = document.createElement('div');
5941
+ wrap_node.classList.add('JXG_wrap_private');
5942
+ wrap_node.setAttribute('id', wrap_id);
5943
+ inner_node.parentNode.insertBefore(wrap_node, inner_node);
5944
+ wrap_node.appendChild(inner_node);
5801
5945
  }
5802
5946
 
5803
- // Start fullscreen mode
5804
- Env.toFullscreen(wrap_id, id);
5947
+ // Get the real width and height of the JSXGraph div
5948
+ // and determine the scaling and vertical shift amount
5949
+ this._fullscreen_res = Env._getScaleFactors(inner_node);
5950
+
5951
+ // Trigger fullscreen mode
5952
+ wrap_node.requestFullscreen = wrap_node.requestFullscreen ||
5953
+ wrap_node.webkitRequestFullscreen ||
5954
+ wrap_node.mozRequestFullScreen ||
5955
+ wrap_node.msRequestFullscreen;
5956
+
5957
+ if (wrap_node.requestFullscreen) {
5958
+ wrap_node.requestFullscreen();
5959
+ }
5805
5960
 
5806
5961
  return this;
5807
5962
  },
@@ -5811,21 +5966,71 @@ define([
5811
5966
  * which are applied to the JSXGraph canvas have to be reread.
5812
5967
  * Otherwise the position of upper left corner is wrongly interpreted.
5813
5968
  *
5814
- * @param {Object} evt fullscreen event object
5969
+ * @param {Object} evt fullscreen event object (unused)
5815
5970
  */
5816
- fullscreenListener: function(evt) {
5817
- var el = this.containerObj;
5971
+ fullscreenListener: function (evt) {
5972
+ var res, inner_id, inner_node;
5973
+
5974
+ inner_id = this._fullscreen_inner_id;
5975
+ if (!Type.exists(inner_id)) {
5976
+ return;
5977
+ }
5818
5978
 
5979
+ document.fullscreenElement = document.fullscreenElement ||
5980
+ document.webkitFullscreenElement ||
5981
+ document.mozFullscreenElement ||
5982
+ document.msFullscreenElement;
5983
+
5984
+ inner_node = document.getElementById(inner_id);
5819
5985
  // If full screen mode is started we have to remove CSS margin around the JSXGraph div.
5820
5986
  // Otherwise, the positioning of the fullscreen div will be false.
5821
5987
  // When leaving the fullscreen mode, the margin is put back in.
5988
+ if (document.fullscreenElement) {
5989
+ // Just entered fullscreen mode
5990
+
5991
+ // Get the data computed in board.toFullscreen()
5992
+ res = this._fullscreen_res;
5993
+
5994
+ // Store the scaling data.
5995
+ // It is used in AbstractRenderer.updateText to restore the scaling matrix
5996
+ // which is removed by MathJax.
5997
+ // Further, the CSS margin has to be removed when in fullscreen mode,
5998
+ // and must be restored later.
5999
+ inner_node._cssFullscreenStore = {
6000
+ id: document.fullscreenElement.id,
6001
+ isFullscreen: true,
6002
+ margin: inner_node.style.margin,
6003
+ width: inner_node.style.width,
6004
+ scale: res.scale,
6005
+ vshift: res.vshift
6006
+ };
6007
+
6008
+ inner_node.style.margin = '';
6009
+ inner_node.style.width = res.width + 'px';
6010
+
6011
+ // Do the shifting and scaling via CSS pseudo rules
6012
+ // We do this after fullscreen mode has been established to get the correct size
6013
+ // of the JSXGraph div.
6014
+ Env.scaleJSXGraphDiv(document.fullscreenElement.id, inner_id, res.scale, res.vshift);
6015
+
6016
+ // Clear document.fullscreenElement, because Safari doesn't to it and
6017
+ // when leaving full screen mode it is still set.
6018
+ document.fullscreenElement = null;
6019
+
6020
+ } else if (Type.exists(inner_node._cssFullscreenStore)) {
6021
+ // Just left the fullscreen mode
6022
+
6023
+ // Remove the CSS rules added in Env.scaleJSXGraphDiv
6024
+ try {
6025
+ document.styleSheets[document.styleSheets.length - 1].deleteRule(0);
6026
+ } catch (err) {
6027
+ console.log('JSXGraph: Could not remove CSS rules for full screen mode');
6028
+ }
6029
+
6030
+ inner_node._cssFullscreenStore.isFullscreen = false;
6031
+ inner_node.style.margin = inner_node._cssFullscreenStore.margin;
6032
+ inner_node.style.width = inner_node._cssFullscreenStore.width;
5822
6033
 
5823
- if (Type.exists(this._cssFullscreenStore) && this._cssFullscreenStore.isFullscreen) {
5824
- el._cssFullscreenStore.isFullscreen = false;
5825
- el.style.margin = this._cssFullscreenStore.margin;
5826
- } else {
5827
- el._cssFullscreenStore.isFullscreen = true;
5828
- el.style.margin = '';
5829
6034
  }
5830
6035
 
5831
6036
  this.updateCSSTransforms();