1 /*
  2     Copyright 2008-2023
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true, window: true*/
 33 /*jslint nomen: true, plusplus: true*/
 34 
 35 /**
 36  * @fileoverview In this file the Text element is defined.
 37  */
 38 
 39 import JXG from "../jxg";
 40 import Const from "./constants";
 41 import GeometryElement from "./element";
 42 import GeonextParser from "../parser/geonext";
 43 import Env from "../utils/env";
 44 import Type from "../utils/type";
 45 import Mat from "../math/math";
 46 import CoordsElement from "./coordselement";
 47 
 48 var priv = {
 49     HTMLSliderInputEventHandler: function () {
 50         this._val = parseFloat(this.rendNodeRange.value);
 51         this.rendNodeOut.value = this.rendNodeRange.value;
 52         this.board.update();
 53     }
 54 };
 55 
 56 /**
 57  * Construct and handle texts.
 58  *
 59  * The coordinates can be relative to the coordinates of an element
 60  * given in {@link JXG.Options#text.anchor}.
 61  *
 62  * MathJax, HTML and GEONExT syntax can be handled.
 63  * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with
 64  * type {@link Text} instead.
 65  * @augments JXG.GeometryElement
 66  * @augments JXG.CoordsElement
 67  * @param {string|JXG.Board} board The board the new text is drawn on.
 68  * @param {Array} coordinates An array with the user coordinates of the text.
 69  * @param {Object} attributes An object containing visual properties and optional a name and a id.
 70  * @param {string|function} content A string or a function returning a string.
 71  *
 72  */
 73 JXG.Text = function (board, coords, attributes, content) {
 74     var tmp;
 75 
 76     this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT);
 77 
 78     this.element = this.board.select(attributes.anchor);
 79     this.coordsConstructor(coords, Type.evaluate(this.visProp.islabel));
 80 
 81     this.content = "";
 82     this.plaintext = "";
 83     this.plaintextOld = null;
 84     this.orgText = "";
 85 
 86     this.needsSizeUpdate = false;
 87     // Only used by infobox anymore
 88     this.hiddenByParent = false;
 89 
 90     /**
 91      * Width and height of the text element in pixel.
 92      *
 93      * @private
 94      * @type Array
 95      */
 96     this.size = [1.0, 1.0];
 97     this.id = this.board.setId(this, "T");
 98 
 99     this.board.renderer.drawText(this);
100     this.board.finalizeAdding(this);
101 
102     // Set text before drawing
103     // this._createFctUpdateText(content);
104     // this.updateText();
105 
106     // Set attribute visible to true. This is necessary to
107     // create all sub-elements for button, input and checkbox
108     tmp = this.visProp.visible;
109     this.visProp.visible = true;
110     this.setText(content);
111     // Restore the correct attribute visible.
112     this.visProp.visible = tmp;
113 
114     if (Type.isString(this.content)) {
115         this.notifyParents(this.content);
116     }
117     this.elType = "text";
118 
119     this.methodMap = Type.deepCopy(this.methodMap, {
120         setText: "setTextJessieCode",
121         // free: 'free',
122         // move: "setCoords"
123     });
124 };
125 
126 JXG.Text.prototype = new GeometryElement();
127 Type.copyPrototypeMethods(JXG.Text, CoordsElement, "coordsConstructor");
128 
129 JXG.extend(
130     JXG.Text.prototype,
131     /** @lends JXG.Text.prototype */ {
132         /**
133          * @private
134          * Test if the screen coordinates (x,y) are in a small stripe
135          * at the left side or at the right side of the text.
136          * Sensitivity is set in this.board.options.precision.hasPoint.
137          * If dragarea is set to 'all' (default), tests if the screen
138          * coordinates (x,y) are in within the text boundary.
139          * @param {Number} x
140          * @param {Number} y
141          * @returns {Boolean}
142          */
143         hasPoint: function (x, y) {
144             var lft, rt, top, bot, ax, ay, type, r;
145 
146             if (Type.isObject(Type.evaluate(this.visProp.precision))) {
147                 type = this.board._inputDevice;
148                 r = Type.evaluate(this.visProp.precision[type]);
149             } else {
150                 // 'inherit'
151                 r = this.board.options.precision.hasPoint;
152             }
153             if (this.transformations.length > 0) {
154                 //Transform the mouse/touch coordinates
155                 // back to the original position of the text.
156                 lft = Mat.matVecMult(
157                     Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)),
158                     [1, x, y]
159                 );
160                 x = lft[1];
161                 y = lft[2];
162             }
163 
164             ax = this.getAnchorX();
165             if (ax === "right") {
166                 lft = this.coords.scrCoords[1] - this.size[0];
167             } else if (ax === "middle") {
168                 lft = this.coords.scrCoords[1] - 0.5 * this.size[0];
169             } else {
170                 lft = this.coords.scrCoords[1];
171             }
172             rt = lft + this.size[0];
173 
174             ay = this.getAnchorY();
175             if (ay === "top") {
176                 bot = this.coords.scrCoords[2] + this.size[1];
177             } else if (ay === "middle") {
178                 bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
179             } else {
180                 bot = this.coords.scrCoords[2];
181             }
182             top = bot - this.size[1];
183 
184             if (Type.evaluate(this.visProp.dragarea) === "all") {
185                 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r;
186             }
187             // e.g. 'small'
188             return (
189                 y >= top - r &&
190                 y <= bot + r &&
191                 ((x >= lft - r && x <= lft + 2 * r) || (x >= rt - 2 * r && x <= rt + r))
192             );
193         },
194 
195         /**
196          * This sets the updateText function of this element depending on the type of text content passed.
197          * Used by {@link JXG.Text#_setText}.
198          * @param {String|Function|Number} text
199          * @private
200          * @see JXG.Text#_setText
201          */
202         _createFctUpdateText: function (text) {
203             var updateText, e, digits,
204                 resolvedText,
205                 i, that,
206                 ev_p = Type.evaluate(this.visProp.parse),
207                 ev_um = Type.evaluate(this.visProp.usemathjax),
208                 ev_uk = Type.evaluate(this.visProp.usekatex),
209                 convertJessieCode = false;
210 
211             this.orgText = text;
212 
213             if (Type.isFunction(text)) {
214                 /**
215                  * Dynamically created function to update the content
216                  * of a text. Can not be overwritten.
217                  * <p>
218                  * <value> tags will not be evaluated if text is provided by a function
219                  * <p>
220                  * Sets the property <tt>plaintext</tt> of the text element.
221                  *
222                  * @private
223                  */
224                 this.updateText = function () {
225                     resolvedText = text().toString(); // Evaluate function
226                     if (ev_p && !ev_um && !ev_uk) {
227                         this.plaintext = this.replaceSub(
228                             this.replaceSup(
229                                 this.convertGeonextAndSketchometry2CSS(resolvedText, false)
230                             )
231                         );
232                     } else {
233                         this.plaintext = resolvedText;
234                     }
235                 };
236             } else {
237                 if (Type.isNumber(text)) {
238                     digits = Type.evaluate(this.visProp.digits);
239                     if (this.useLocale()) {
240                         this.content = this.formatNumberLocale(text, digits);
241                     } else {
242                         this.content = Type.toFixed(text, digits);
243                     }
244                 } else if (Type.isString(text) && ev_p) {
245                     if (Type.evaluate(this.visProp.useasciimathml)) {
246                         // ASCIIMathML
247                         // value-tags are not supported
248                         this.content = "'`" + text + "`'";
249                     } else if (ev_um || ev_uk) {
250                         // MathJax or KaTeX
251                         // Replace value-tags by functions
252                         // sketchofont is ignored
253                         this.content = this.valueTagToJessieCode(text);
254                         if (!Type.isArray(this.content)) {
255                             // For some reason we don't have to mask backslashes in an array of strings
256                             // anymore.
257                             //
258                             // for (i = 0; i < this.content.length; i++) {
259                             //     this.content[i] = this.content[i].replace(/\\/g, "\\\\"); // Replace single backslash by double
260                             // }
261                             // } else {
262                             this.content = this.content.replace(/\\/g, "\\\\"); // Replace single backslash by double
263                         }
264                     } else {
265                         // No TeX involved.
266                         // Converts GEONExT syntax into JavaScript string
267                         // Short math is allowed
268                         // Replace value-tags by functions
269                         // Avoid geonext2JS calls
270                         this.content = this.poorMansTeX(this.valueTagToJessieCode(text));
271                     }
272                     convertJessieCode = true;
273                 } else {
274                     this.content = text;
275                 }
276 
277                 // Generate function which returns the text to be displayed
278                 if (convertJessieCode) {
279                     // Convert JessieCode to JS function
280                     if (Type.isArray(this.content)) {
281                         // This is the case if the text contained value-tags.
282                         // These value-tags consist of JessieCode snippets
283                         // which are now replaced by JavaScript functions
284                         that = this;
285                         for (i = 0; i < this.content.length; i++) {
286                             if (this.content[i][0] !== '"') {
287                                 this.content[i] = this.board.jc.snippet(this.content[i], true, "", false);
288                                 for (e in this.content[i].deps) {
289                                     this.addParents(this.content[i].deps[e]);
290                                     this.content[i].deps[e].addChild(this);
291                                 }
292                             }
293                         }
294 
295                         updateText = function() {
296                             var i, t,
297                                 digits = Type.evaluate(that.visProp.digits),
298                                 txt = '';
299 
300                             for (i = 0; i < that.content.length; i++) {
301                                 if (Type.isFunction(that.content[i])) {
302                                     t = that.content[i]();
303                                     if (that.useLocale()) {
304                                         t = that.formatNumberLocale(t, digits);
305                                     } else {
306                                         t = Type.toFixed(t, digits);
307                                     }
308                                 } else {
309                                     t = that.content[i];
310                                     if (t.at(0) === '"' && t.at(-1) === '"') {
311                                         t = t.slice(1, -1);
312                                     }
313                                 }
314 
315                                 txt += t;
316                             }
317                             return txt;
318                         };
319                     } else {
320                         updateText = this.board.jc.snippet(this.content, true, "", false);
321                         for (e in updateText.deps) {
322                             this.addParents(updateText.deps[e]);
323                             updateText.deps[e].addChild(this);
324                         }
325                     }
326 
327                     // Ticks have been escaped in valueTagToJessieCode
328                     this.updateText = function () {
329                         this.plaintext = this.unescapeTicks(updateText());
330                     };
331                 } else {
332                     this.updateText = function () {
333                         this.plaintext = this.content; // text;
334                     };
335                 }
336             }
337         },
338 
339         /**
340          * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
341          * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
342          * @param {String|Function|Number} text
343          * @returns {JXG.Text}
344          * @private
345          */
346         _setText: function (text) {
347             this._createFctUpdateText(text);
348 
349             // First evaluation of the string.
350             // We need this for display='internal' and Canvas
351             this.updateText();
352             this.fullUpdate();
353 
354             // We do not call updateSize for the infobox to speed up rendering
355             if (!this.board.infobox || this.id !== this.board.infobox.id) {
356                 this.updateSize(); // updateSize() is called at least once.
357             }
358 
359             // This may slow down canvas renderer
360             // if (this.board.renderer.type === 'canvas') {
361             //     this.board.fullUpdate();
362             // }
363 
364             return this;
365         },
366 
367         /**
368          * Defines new content but converts < and > to HTML entities before updating the DOM.
369          * @param {String|function} text
370          */
371         setTextJessieCode: function (text) {
372             var s;
373 
374             this.visProp.castext = text;
375             if (Type.isFunction(text)) {
376                 s = function () {
377                     return Type.sanitizeHTML(text());
378                 };
379             } else {
380                 if (Type.isNumber(text)) {
381                     s = text;
382                 } else {
383                     s = Type.sanitizeHTML(text);
384                 }
385             }
386 
387             return this._setText(s);
388         },
389 
390         /**
391          * Defines new content.
392          * @param {String|function} text
393          * @returns {JXG.Text} Reference to the text object.
394          */
395         setText: function (text) {
396             return this._setText(text);
397         },
398 
399         /**
400          * Recompute the width and the height of the text box.
401          * Updates the array {@link JXG.Text#size} with pixel values.
402          * The result may differ from browser to browser
403          * by some pixels.
404          * In canvas an old IEs we use a very crude estimation of the dimensions of
405          * the textbox.
406          * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and
407          * for aligning text.
408          *
409          * @return {[type]} [description]
410          */
411         updateSize: function () {
412             var tmp,
413                 that,
414                 node,
415                 ev_d = Type.evaluate(this.visProp.display);
416 
417             if (!Env.isBrowser || this.board.renderer.type === "no") {
418                 return this;
419             }
420             node = this.rendNode;
421 
422             /**
423              * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode.
424              */
425             if (ev_d === "html" || this.board.renderer.type === "vml") {
426                 if (Type.exists(node.offsetWidth)) {
427                     that = this;
428                     window.setTimeout(function () {
429                         that.size = [node.offsetWidth, node.offsetHeight];
430                         that.needsUpdate = true;
431                         that.updateRenderer();
432                     }, 0);
433                     // In case, there is non-zero padding or borders
434                     // the following approach does not longer work.
435                     // s = [node.offsetWidth, node.offsetHeight];
436                     // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight
437                     //     that = this;
438                     //     window.setTimeout(function () {
439                     //         that.size = [node.offsetWidth, node.offsetHeight];
440                     //         that.needsUpdate = true;
441                     //         that.updateRenderer();
442                     //     }, 0);
443                     // } else {
444                     //     this.size = s;
445                     // }
446                 } else {
447                     this.size = this.crudeSizeEstimate();
448                 }
449             } else if (ev_d === "internal") {
450                 if (this.board.renderer.type === "svg") {
451                     that = this;
452                     window.setTimeout(function () {
453                         try {
454                             tmp = node.getBBox();
455                             that.size = [tmp.width, tmp.height];
456                             that.needsUpdate = true;
457                             that.updateRenderer();
458                         } catch (e) {}
459                     }, 0);
460                 } else if (this.board.renderer.type === "canvas") {
461                     this.size = this.crudeSizeEstimate();
462                 }
463             }
464 
465             return this;
466         },
467 
468         /**
469          * A very crude estimation of the dimensions of the textbox in case nothing else is available.
470          * @returns {Array}
471          */
472         crudeSizeEstimate: function () {
473             var ev_fs = parseFloat(Type.evaluate(this.visProp.fontsize));
474             return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9];
475         },
476 
477         /**
478          * Decode unicode entities into characters.
479          * @param {String} string
480          * @returns {String}
481          */
482         utf8_decode: function (string) {
483             return string.replace(/&#x(\w+);/g, function (m, p1) {
484                 return String.fromCharCode(parseInt(p1, 16));
485             });
486         },
487 
488         /**
489          * Replace _{} by <sub>
490          * @param {String} te String containing _{}.
491          * @returns {String} Given string with _{} replaced by <sub>.
492          */
493         replaceSub: function (te) {
494             if (!te.indexOf) {
495                 return te;
496             }
497 
498             var j,
499                 i = te.indexOf("_{");
500 
501             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
502             // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway.
503             /*jslint regexp: true*/
504 
505             while (i >= 0) {
506                 te = te.substr(0, i) + te.substr(i).replace(/_\{/, "<sub>");
507                 j = te.substr(i).indexOf("}");
508                 if (j >= 0) {
509                     te = te.substr(0, j) + te.substr(j).replace(/\}/, "</sub>");
510                 }
511                 i = te.indexOf("_{");
512             }
513 
514             i = te.indexOf("_");
515             while (i >= 0) {
516                 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, "<sub>$1</sub>");
517                 i = te.indexOf("_");
518             }
519 
520             return te;
521         },
522 
523         /**
524          * Replace ^{} by <sup>
525          * @param {String} te String containing ^{}.
526          * @returns {String} Given string with ^{} replaced by <sup>.
527          */
528         replaceSup: function (te) {
529             if (!te.indexOf) {
530                 return te;
531             }
532 
533             var j,
534                 i = te.indexOf("^{");
535 
536             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
537             // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway.
538             /*jslint regexp: true*/
539 
540             while (i >= 0) {
541                 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, "<sup>");
542                 j = te.substr(i).indexOf("}");
543                 if (j >= 0) {
544                     te = te.substr(0, j) + te.substr(j).replace(/\}/, "</sup>");
545                 }
546                 i = te.indexOf("^{");
547             }
548 
549             i = te.indexOf("^");
550             while (i >= 0) {
551                 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, "<sup>$1</sup>");
552                 i = te.indexOf("^");
553             }
554 
555             return te;
556         },
557 
558         /**
559          * Return the width of the text element.
560          * @returns {Array} [width, height] in pixel
561          */
562         getSize: function () {
563             return this.size;
564         },
565 
566         /**
567          * Move the text to new coordinates.
568          * @param {number} x
569          * @param {number} y
570          * @returns {object} reference to the text object.
571          */
572         setCoords: function (x, y) {
573             var coordsAnchor, dx, dy;
574             if (Type.isArray(x) && x.length > 1) {
575                 y = x[1];
576                 x = x[0];
577             }
578 
579             if (Type.evaluate(this.visProp.islabel) && Type.exists(this.element)) {
580                 coordsAnchor = this.element.getLabelAnchor();
581                 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX;
582                 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY;
583 
584                 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]);
585             } else {
586                 /*
587                 this.X = function () {
588                     return x;
589                 };
590 
591                 this.Y = function () {
592                     return y;
593                 };
594                 */
595                 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]);
596             }
597 
598             // this should be a local update, otherwise there might be problems
599             // with the tick update routine resulting in orphaned tick labels
600             this.fullUpdate();
601 
602             return this;
603         },
604 
605         /**
606          * Evaluates the text.
607          * Then, the update function of the renderer
608          * is called.
609          */
610         update: function (fromParent) {
611             if (!this.needsUpdate) {
612                 return this;
613             }
614 
615             this.updateCoords(fromParent);
616             this.updateText();
617 
618             if (Type.evaluate(this.visProp.display) === "internal") {
619                 if (Type.isString(this.plaintext)) {
620                     this.plaintext = this.utf8_decode(this.plaintext);
621                 }
622             }
623 
624             this.checkForSizeUpdate();
625             if (this.needsSizeUpdate) {
626                 this.updateSize();
627             }
628 
629             return this;
630         },
631 
632         /**
633          * Used to save updateSize() calls.
634          * Called in JXG.Text.update
635          * That means this.update() has been called.
636          * More tests are in JXG.Renderer.updateTextStyle. The latter tests
637          * are one update off. But this should pose not too many problems, since
638          * it affects fontSize and cssClass changes.
639          *
640          * @private
641          */
642         checkForSizeUpdate: function () {
643             if (this.board.infobox && this.id === this.board.infobox.id) {
644                 this.needsSizeUpdate = false;
645             } else {
646                 // For some magic reason it is more efficient on the iPad to
647                 // call updateSize() for EVERY text element EVERY time.
648                 this.needsSizeUpdate = this.plaintextOld !== this.plaintext;
649 
650                 if (this.needsSizeUpdate) {
651                     this.plaintextOld = this.plaintext;
652                 }
653             }
654         },
655 
656         /**
657          * The update function of the renderert
658          * is called.
659          * @private
660          */
661         updateRenderer: function () {
662             if (
663                 //this.board.updateQuality === this.board.BOARD_QUALITY_HIGH &&
664                 Type.evaluate(this.visProp.autoposition)
665             ) {
666                 this.setAutoPosition().updateConstraint();
667             }
668             return this.updateRendererGeneric("updateText");
669         },
670 
671         /**
672          * Converts shortened math syntax into correct syntax:  3x instead of 3*x or
673          * (a+b)(3+1) instead of (a+b)*(3+1).
674          *
675          * @private
676          * @param{String} expr Math term
677          * @returns {string} expanded String
678          */
679         expandShortMath: function (expr) {
680             var re = /([)0-9.])\s*([(a-zA-Z_])/g;
681             return expr.replace(re, "$1*$2");
682         },
683 
684         /**
685          * Converts the GEONExT syntax of the <value> terms into JavaScript.
686          * Also, all Objects whose name appears in the term are searched and
687          * the text is added as child to these objects.
688          * This method is called if the attribute parse==true is set.
689          *
690          * Obsolete, replaced by JXG.Text.valueTagToJessieCode
691          *
692          * @param{String} contentStr String to be parsed
693          * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x).
694          * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility
695          * this has to be set explicitely to true.
696          * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX.
697          * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done.
698          * This flag allows the combination of <value> tag containing calculations with TeX output.
699          *
700          * @deprecated
701          * @private
702          * @see JXG.GeonextParser#geonext2JS
703          * @see JXG.Text#valueTagToJessieCode
704          *
705          */
706         generateTerm: function (contentStr, expand, avoidGeonext2JS) {
707             var res,
708                 term,
709                 i,
710                 j,
711                 plaintext = '""';
712 
713             // Revert possible jc replacement
714             contentStr = contentStr || "";
715             contentStr = contentStr.replace(/\r/g, "");
716             contentStr = contentStr.replace(/\n/g, "");
717             contentStr = contentStr.replace(/"/g, "'");
718             contentStr = contentStr.replace(/'/g, "\\'");
719 
720             // Old GEONExT syntax, not (yet) supported as TeX output.
721             // Otherwise, the else clause should be used.
722             // That means, i.e. the <arc> tag and <sqrt> tag are not
723             // converted into TeX syntax.
724             contentStr = contentStr.replace(/&arc;/g, "∠");
725             contentStr = contentStr.replace(/<arc\s*\/>/g, "∠");
726             contentStr = contentStr.replace(/<arc\s*\/>/g, "∠");
727             contentStr = contentStr.replace(/<sqrt\s*\/>/g, "√");
728 
729             contentStr = contentStr.replace(/<value>/g, "<value>");
730             contentStr = contentStr.replace(/<\/value>/g, "</value>");
731 
732             // Convert GEONExT syntax into  JavaScript syntax
733             i = contentStr.indexOf("<value>");
734             j = contentStr.indexOf("</value>");
735             if (i >= 0) {
736                 while (i >= 0) {
737                     plaintext +=
738                         ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"';
739                     // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"';
740 
741                     term = contentStr.slice(i + 7, j);
742                     term = term.replace(/\s+/g, ""); // Remove all whitespace
743                     if (expand === true) {
744                         term = this.expandShortMath(term);
745                     }
746                     if (avoidGeonext2JS) {
747                         res = term;
748                     } else {
749                         res = GeonextParser.geonext2JS(term, this.board);
750                     }
751                     res = res.replace(/\\"/g, "'");
752                     res = res.replace(/\\'/g, "'");
753 
754                     // GEONExT-Hack: apply rounding once only.
755                     if (res.indexOf("toFixed") < 0) {
756                         // output of a value tag
757                         if (
758                             Type.isNumber(
759                                 Type.bind(this.board.jc.snippet(res, true, '', false), this)()
760                             )
761                         ) {
762                             // may also be a string
763                             plaintext += '+(' + res + ').toFixed(' + Type.evaluate(this.visProp.digits) + ')';
764                         } else {
765                             plaintext += '+(' + res + ')';
766                         }
767                     } else {
768                         plaintext += '+(' + res + ')';
769                     }
770 
771                     contentStr = contentStr.slice(j + 8);
772                     i = contentStr.indexOf("<value>");
773                     j = contentStr.indexOf("</value>");
774                 }
775             }
776 
777             plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"';
778             plaintext = this.convertGeonextAndSketchometry2CSS(plaintext);
779 
780             // This should replace e.g. &pi; by π
781             plaintext = plaintext.replace(/&/g, "&");
782             plaintext = plaintext.replace(/"/g, "'");
783 
784             return plaintext;
785         },
786 
787         /**
788          * Replace value-tags in string by JessieCode functions.
789          * @param {String} contentStr
790          * @returns String
791          * @private
792          * @example
793          * "The x-coordinate of A is <value>X(A)</value>"
794          *
795          */
796         valueTagToJessieCode: function (contentStr) {
797             var res, term,
798                 i, j,
799                 expandShortMath = true,
800                 textComps = [],
801                 tick = '"';
802 
803             contentStr = contentStr || "";
804             contentStr = contentStr.replace(/\r/g, "");
805             contentStr = contentStr.replace(/\n/g, "");
806 
807             contentStr = contentStr.replace(/<value>/g, "<value>");
808             contentStr = contentStr.replace(/<\/value>/g, "</value>");
809 
810             // Convert content of value tag (GEONExT/JessieCode) syntax into JavaScript syntax
811             i = contentStr.indexOf("<value>");
812             j = contentStr.indexOf("</value>");
813             if (i >= 0) {
814                 while (i >= 0) {
815                     // Add string fragment before <value> tag
816                     textComps.push(tick + this.escapeTicks(contentStr.slice(0, i)) + tick);
817 
818                     term = contentStr.slice(i + 7, j);
819                     term = term.replace(/\s+/g, ""); // Remove all whitespace
820                     if (expandShortMath === true) {
821                         term = this.expandShortMath(term);
822                     }
823                     res = term;
824                     res = res.replace(/\\"/g, "'").replace(/\\'/g, "'");
825 
826                     // // Hack: apply rounding once only.
827                     // if (res.indexOf("toFixed") < 0) {
828                     //     // Output of a value tag
829                     //     // Run the JessieCode parser
830                     //     if (
831                     //         Type.isNumber(
832                     //             Type.bind(this.board.jc.snippet(res, true, "", false), this)()
833                     //         )
834                     //     ) {
835                     //         // Output is number
836                     //         // textComps.push(
837                     //         //     '(' + res + ').toFixed(' + Type.evaluate(this.visProp.digits) + ')'
838                     //         // );
839                     //         textComps.push('(' + res + ')');
840                     //     } else {
841                     //         // Output is a string
842                     //         textComps.push("(" + res + ")");
843                     //     }
844                     // } else {
845                         textComps.push("(" + res + ")");
846                     // }
847                     contentStr = contentStr.slice(j + 8);
848                     i = contentStr.indexOf("<value>");
849                     j = contentStr.indexOf("</value>");
850                 }
851             }
852             // Add trailing string fragment
853             textComps.push(tick + this.escapeTicks(contentStr) + tick);
854 
855             // return textComps.join(" + ").replace(/&/g, "&");
856             for (i = 0; i < textComps.length; i++) {
857                 textComps[i] = textComps[i].replace(/&/g, "&");
858             }
859             return textComps;
860         },
861 
862         /**
863          * Simple math rendering using HTML / CSS only. In case of array,
864          * handle each entry separately and return array with the
865          * rendering strings.
866          *
867          * @param {String|Array} s
868          * @returns {String|Array}
869          * @see JXG.Text#convertGeonextAndSketchometry2CSS
870          * @private
871          * @see JXG.Text#replaceSub
872          * @see JXG.Text#replaceSup
873          * @see JXG.Text#convertGeonextAndSketchometry2CSS
874          */
875         poorMansTeX: function (s) {
876             var i, a;
877             if (Type.isArray(s)) {
878                 a = [];
879                 for (i = 0; i < s.length; i++) {
880                     a.push(this.poorMansTeX(s[i]));
881                 }
882                 return a;
883             }
884 
885             s = s
886                 .replace(/<arc\s*\/*>/g, "∠")
887                 .replace(/<arc\s*\/*>/g, "∠")
888                 .replace(/<sqrt\s*\/*>/g, "√")
889                 .replace(/<sqrt\s*\/*>/g, "√");
890             return this.convertGeonextAndSketchometry2CSS(this.replaceSub(this.replaceSup(s)), true);
891         },
892 
893         /**
894          * Replace ticks by URI escape sequences
895          *
896          * @param {String} s
897          * @returns String
898          * @private
899          *
900          */
901         escapeTicks: function (s) {
902             return s.replace(/"/g, "%22").replace(/'/g, "%27");
903         },
904 
905         /**
906          * Replace escape sequences for ticks by ticks
907          *
908          * @param {String} s
909          * @returns String
910          * @private
911          */
912         unescapeTicks: function (s) {
913             return s.replace(/%22/g, '"').replace(/%27/g, "'");
914         },
915 
916         /**
917          * Converts the GEONExT tags <overline> and <arrow> to
918          * HTML span tags with proper CSS formatting.
919          * @private
920          * @see JXG.Text.poorMansTeX
921          * @see JXG.Text._setText
922          */
923         convertGeonext2CSS: function (s) {
924             if (Type.isString(s)) {
925                 s = s.replace(
926                     /(<|<)overline(>|>)/g,
927                     "<span style=text-decoration:overline;>"
928                 );
929                 s = s.replace(/(<|<)\/overline(>|>)/g, "</span>");
930                 s = s.replace(
931                     /(<|<)arrow(>|>)/g,
932                     "<span style=text-decoration:overline;>"
933                 );
934                 s = s.replace(/(<|<)\/arrow(>|>)/g, "</span>");
935             }
936 
937             return s;
938         },
939 
940         /**
941          * Converts the sketchometry tag <sketchofont> to
942          * HTML span tags with proper CSS formatting.
943          *
944          * @param {String|Function|Number} s Text
945          * @param {Boolean} escape Flag if ticks should be escaped. Escaping is necessary
946          * if s is a text. It has to be avoided if s is a function returning text.
947          * @private
948          * @see JXG.Text._setText
949          * @see JXG.Text.convertGeonextAndSketchometry2CSS
950          *
951          */
952         convertSketchometry2CSS: function (s, escape) {
953             var t1 = "<span class=\"sketcho sketcho-inherit sketcho-",
954                 t2 = "\"></span>";
955 
956             if (Type.isString(s)) {
957                 if (escape) {
958                     t1 = this.escapeTicks(t1);
959                     t2 = this.escapeTicks(t2);
960                 }
961                 s = s.replace(/(<|<)sketchofont(>|>)/g, t1);
962                 s = s.replace(/(<|<)\/sketchofont(>|>)/g, t2);
963             }
964 
965             return s;
966         },
967 
968         /**
969          * Alias for convertGeonext2CSS and convertSketchometry2CSS
970          *
971          * @param {String|Function|Number} s Text
972          * @param {Boolean} escape Flag if ticks should be escaped
973          * @private
974          * @see JXG.Text.convertGeonext2CSS
975          * @see JXG.Text.convertSketchometry2CSS
976          */
977         convertGeonextAndSketchometry2CSS: function (s, escape) {
978             s = this.convertGeonext2CSS(s);
979             s = this.convertSketchometry2CSS(s, escape);
980             return s;
981         },
982 
983         /**
984          * Finds dependencies in a given term and notifies the parents by adding the
985          * dependent object to the found objects child elements.
986          * @param {String} content String containing dependencies for the given object.
987          * @private
988          */
989         notifyParents: function (content) {
990             var search,
991                 res = null;
992 
993             // revert possible jc replacement
994             content = content.replace(/<value>/g, "<value>");
995             content = content.replace(/<\/value>/g, "</value>");
996 
997             do {
998                 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/;
999                 res = search.exec(content);
1000 
1001                 if (res !== null) {
1002                     GeonextParser.findDependencies(this, res[1], this.board);
1003                     content = content.substr(res.index);
1004                     content = content.replace(search, "");
1005                 }
1006             } while (res !== null);
1007 
1008             return this;
1009         },
1010 
1011         // documented in element.js
1012         getParents: function () {
1013             var p;
1014             if (this.relativeCoords !== undefined) {
1015                 // Texts with anchor elements, excluding labels
1016                 p = [
1017                     this.relativeCoords.usrCoords[1],
1018                     this.relativeCoords.usrCoords[2],
1019                     this.orgText
1020                 ];
1021             } else {
1022                 // Other texts
1023                 p = [this.Z(), this.X(), this.Y(), this.orgText];
1024             }
1025 
1026             if (this.parents.length !== 0) {
1027                 p = this.parents;
1028             }
1029 
1030             return p;
1031         },
1032 
1033         /**
1034          * Returns the bounding box of the text element in user coordinates as an
1035          * array of length 4: [upper left x, upper left y, lower right x, lower right y].
1036          * The method assumes that the lower left corner is at position [el.X(), el.Y()]
1037          * of the text element el, i.e. the attributes anchorX, anchorY are ignored.
1038          *
1039          * <p>
1040          * or labels, [0, 0, 0, 0] is returned.
1041          *
1042          * @returns Array
1043          */
1044         bounds: function () {
1045             var c = this.coords.usrCoords;
1046 
1047             if (
1048                 Type.evaluate(this.visProp.islabel) ||
1049                 this.board.unitY === 0 ||
1050                 this.board.unitX === 0
1051             ) {
1052                 return [0, 0, 0, 0];
1053             }
1054             return [
1055                 c[1],
1056                 c[2] + this.size[1] / this.board.unitY,
1057                 c[1] + this.size[0] / this.board.unitX,
1058                 c[2]
1059             ];
1060         },
1061 
1062         /**
1063          * Returns the value of the attribute "anchorX". If this equals "auto",
1064          * returns "left", "middle", or "right", depending on the
1065          * value of the attribute "position".
1066          * @returns String
1067          */
1068         getAnchorX: function () {
1069             var a = Type.evaluate(this.visProp.anchorx);
1070             if (a === "auto") {
1071                 switch (this.visProp.position) {
1072                     case "top":
1073                     case "bot":
1074                         return "middle";
1075                     case "rt":
1076                     case "lrt":
1077                     case "urt":
1078                         return "left";
1079                     case "lft":
1080                     case "llft":
1081                     case "ulft":
1082                     default:
1083                         return "right";
1084                 }
1085             }
1086             return a;
1087         },
1088 
1089         /**
1090          * Returns the value of the attribute "anchorY". If this equals "auto",
1091          * returns "bottom", "middle", or "top", depending on the
1092          * value of the attribute "position".
1093          * @returns String
1094          */
1095         getAnchorY: function () {
1096             var a = Type.evaluate(this.visProp.anchory);
1097             if (a === "auto") {
1098                 switch (this.visProp.position) {
1099                     case "top":
1100                     case "ulft":
1101                     case "urt":
1102                         return "bottom";
1103                     case "bot":
1104                     case "lrt":
1105                     case "llft":
1106                         return "top";
1107                     case "rt":
1108                     case "lft":
1109                     default:
1110                         return "middle";
1111                 }
1112             }
1113             return a;
1114         },
1115 
1116         /**
1117          * Computes the number of overlaps of a box of w pixels width, h pixels height
1118          * and center (x, y)
1119          *
1120          * @private
1121          * @param  {Number} x x-coordinate of the center (screen coordinates)
1122          * @param  {Number} y y-coordinate of the center (screen coordinates)
1123          * @param  {Number} w width of the box in pixel
1124          * @param  {Number} h width of the box in pixel
1125          * @return {Number}   Number of overlapping elements
1126          */
1127         getNumberofConflicts: function (x, y, w, h) {
1128             var count = 0,
1129                 i,
1130                 obj,
1131                 le,
1132                 savePointPrecision;
1133 
1134             // Set the precision of hasPoint to half the max if label isn't too long
1135             savePointPrecision = this.board.options.precision.hasPoint;
1136             // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5;
1137             this.board.options.precision.hasPoint = (w + h) * 0.25;
1138             // TODO:
1139             // Make it compatible with the objects' visProp.precision attribute
1140             for (i = 0, le = this.board.objectsList.length; i < le; i++) {
1141                 obj = this.board.objectsList[i];
1142                 if (
1143                     obj.visPropCalc.visible &&
1144                     obj.elType !== "axis" &&
1145                     obj.elType !== "ticks" &&
1146                     obj !== this.board.infobox &&
1147                     obj !== this &&
1148                     obj.hasPoint(x, y)
1149                 ) {
1150                     count++;
1151                 }
1152             }
1153             this.board.options.precision.hasPoint = savePointPrecision;
1154 
1155             return count;
1156         },
1157 
1158         /**
1159          * Sets the offset of a label element to the position with the least number
1160          * of overlaps with other elements, while retaining the distance to its
1161          * anchor element. Twelve different angles are possible.
1162          *
1163          * @returns {JXG.Text} Reference to the text object.
1164          */
1165         setAutoPosition: function () {
1166             var x,
1167                 y,
1168                 cx,
1169                 cy,
1170                 anchorCoords,
1171                 // anchorX, anchorY,
1172                 w = this.size[0],
1173                 h = this.size[1],
1174                 start_angle,
1175                 angle,
1176                 optimum = {
1177                     conflicts: Infinity,
1178                     angle: 0,
1179                     r: 0
1180                 },
1181                 max_r,
1182                 delta_r,
1183                 conflicts,
1184                 offset,
1185                 r,
1186                 num_positions = 12,
1187                 step = (2 * Math.PI) / num_positions,
1188                 j,
1189                 dx,
1190                 dy,
1191                 co,
1192                 si;
1193 
1194             if (
1195                 this === this.board.infobox ||
1196                 !this.visPropCalc.visible ||
1197                 !Type.evaluate(this.visProp.islabel) ||
1198                 !this.element
1199             ) {
1200                 return this;
1201             }
1202 
1203             // anchorX = Type.evaluate(this.visProp.anchorx);
1204             // anchorY = Type.evaluate(this.visProp.anchory);
1205             offset = Type.evaluate(this.visProp.offset);
1206             anchorCoords = this.element.getLabelAnchor();
1207             cx = anchorCoords.scrCoords[1];
1208             cy = anchorCoords.scrCoords[2];
1209 
1210             // Set dx, dy as the relative position of the center of the label
1211             // to its anchor element ignoring anchorx and anchory.
1212             dx = offset[0];
1213             dy = offset[1];
1214 
1215             conflicts = this.getNumberofConflicts(cx + dx, cy - dy, w, h);
1216             if (conflicts === 0) {
1217                 return this;
1218             }
1219             // console.log(this.id, conflicts, w, h);
1220             // r = Geometry.distance([0, 0], offset, 2);
1221 
1222             r = 12;
1223             max_r = 28;
1224             delta_r = 0.2 * r;
1225 
1226             start_angle = Math.atan2(dy, dx);
1227 
1228             optimum.conflicts = conflicts;
1229             optimum.angle = start_angle;
1230             optimum.r = r;
1231 
1232             while (optimum.conflicts > 0 && r < max_r) {
1233                 for (
1234                     j = 1, angle = start_angle + step;
1235                     j < num_positions && optimum.conflicts > 0;
1236                     j++
1237                 ) {
1238                     co = Math.cos(angle);
1239                     si = Math.sin(angle);
1240 
1241                     x = cx + r * co;
1242                     y = cy - r * si;
1243 
1244                     conflicts = this.getNumberofConflicts(x, y, w, h);
1245                     if (conflicts < optimum.conflicts) {
1246                         optimum.conflicts = conflicts;
1247                         optimum.angle = angle;
1248                         optimum.r = r;
1249                     }
1250                     if (optimum.conflicts === 0) {
1251                         break;
1252                     }
1253                     angle += step;
1254                 }
1255                 r += delta_r;
1256             }
1257             // console.log(this.id, "after", optimum)
1258             r = optimum.r;
1259             co = Math.cos(optimum.angle);
1260             si = Math.sin(optimum.angle);
1261             this.visProp.offset = [r * co, r * si];
1262 
1263             if (co < -0.2) {
1264                 this.visProp.anchorx = "right";
1265             } else if (co > 0.2) {
1266                 this.visProp.anchorx = "left";
1267             } else {
1268                 this.visProp.anchorx = "middle";
1269             }
1270 
1271             return this;
1272         }
1273     }
1274 );
1275 
1276 /**
1277  * @class Construct and handle texts.
1278  *
1279  * The coordinates can either be abslute (i.e. respective to the coordinate system of the board) or be relative to the coordinates of an element
1280  * given in {@link Text#anchor}.
1281  * <p>
1282  * HTML, MathJaX, KaTeX and GEONExT syntax can be handled.
1283  * <p>
1284  * There are two ways to display texts:
1285  * <ul>
1286  * <li> using the text element of the renderer (canvas or svg). In most cases this is the suitable approach if speed matters.
1287  * However, advanced rendering like MathJax, KaTeX or HTML/CSS are not possible.
1288  * <li> using HTML <div>. This is the most flexible approach. The drawback is that HTML can only be display "above" the geometry elements.
1289  * If HTML should be displayed in an inbetween layer, conder to use an element of type {@link ForeignObject} (available in svg renderer, only).
1290  * </ul>
1291  * @pseudo
1292  * @description
1293  * @name Text
1294  * @augments JXG.Text
1295  * @constructor
1296  * @type JXG.Text
1297  *
1298  * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements.
1299  *                     <p>
1300  *   Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T
1301  *   constraint, or a function which takes no parameter and returns a number. Every parent element beside the last determines one coordinate.
1302  *   If a coordinate is
1303  *   given by a number, the number determines the initial position of a free text. If given by a string or a function that coordinate will be constrained
1304  *   that means the user won't be able to change the texts's position directly by mouse because it will be calculated automatically depending on the string
1305  *   or the function's return value. If two parent elements are given the coordinates will be interpreted as 2D affine Euclidean coordinates, if three such
1306  *   parent elements are given they will be interpreted as homogeneous coordinates.
1307  *                     <p>
1308  *                     The text to display may be given as string or as function returning a string.
1309  *
1310  * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' an HTML division tag is created to display
1311  * the text. In this case it is also possible to use MathJax, KaTeX, or ASCIIMathML. If neither of these is used, basic Math rendering is
1312  * applied.
1313  * <p>
1314  * In case of 'internal', an SVG text element is used to display the text.
1315  * @see JXG.Text
1316  * @example
1317  * // Create a fixed text at position [0,1].
1318  *   var t1 = board.create('text',[0,1,"Hello World"]);
1319  * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
1320  * <script type="text/javascript">
1321  *   var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
1322  *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
1323  * </script><pre>
1324  * @example
1325  * // Create a variable text at a variable position.
1326  *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
1327  *   var graph = board.create('text',
1328  *                        [function(x){ return s.Value();}, 1,
1329  *                         function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);}
1330  *                        ]
1331  *                     );
1332  * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
1333  * <script type="text/javascript">
1334  *   var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
1335  *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
1336  *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]);
1337  * </script><pre>
1338  * @example
1339  * // Create a text bound to the point A
1340  * var p = board.create('point',[0, 1]),
1341  *     t = board.create('text',[0, -1,"Hello World"], {anchor: p});
1342  *
1343  * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div>
1344  * <script type="text/javascript">
1345  *     (function() {
1346  *         var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723',
1347  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
1348  *     var p = board.create('point',[0, 1]),
1349  *         t = board.create('text',[0, -1,"Hello World"], {anchor: p});
1350  *
1351  *     })();
1352  *
1353  * </script><pre>
1354  *
1355  */
1356 JXG.createText = function (board, parents, attributes) {
1357     var t,
1358         attr = Type.copyAttributes(attributes, board.options, "text"),
1359         coords = parents.slice(0, -1),
1360         content = parents[parents.length - 1];
1361 
1362     // downwards compatibility
1363     attr.anchor = attr.parent || attr.anchor;
1364     t = CoordsElement.create(JXG.Text, board, coords, attr, content);
1365 
1366     if (!t) {
1367         throw new Error(
1368             "JSXGraph: Can't create text with parent types '" +
1369                 typeof parents[0] +
1370                 "' and '" +
1371                 typeof parents[1] +
1372                 "'." +
1373                 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]"
1374         );
1375     }
1376 
1377     if (attr.rotate !== 0) {
1378         // This is the default value, i.e. no rotation
1379         t.addRotation(attr.rotate);
1380     }
1381 
1382     return t;
1383 };
1384 
1385 JXG.registerElement("text", JXG.createText);
1386 
1387 /**
1388  * @class Labels are text objects tied to other elements like points, lines and curves.
1389  * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)".
1390  *
1391  * @pseudo
1392  * @description
1393  * @name Label
1394  * @augments JXG.Text
1395  * @constructor
1396  * @type JXG.Text
1397  */
1398 //  See element.js#createLabel
1399 
1400 /**
1401  * [[x,y], [w px, h px], [range]
1402  */
1403 JXG.createHTMLSlider = function (board, parents, attributes) {
1404     var t,
1405         par,
1406         attr = Type.copyAttributes(attributes, board.options, "htmlslider");
1407 
1408     if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) {
1409         throw new Error(
1410             "JSXGraph: Can't create htmlslider with parent types '" +
1411                 typeof parents[0] +
1412                 "' and '" +
1413                 typeof parents[1] +
1414                 "'." +
1415                 "\nPossible parents are: [[x,y], [min, start, max]]"
1416         );
1417     }
1418 
1419     // backwards compatibility
1420     attr.anchor = attr.parent || attr.anchor;
1421     attr.fixed = attr.fixed || true;
1422 
1423     par = [
1424         parents[0][0],
1425         parents[0][1],
1426         '<form style="display:inline">' +
1427             '<input type="range" /><span></span><input type="text" />' +
1428             "</form>"
1429     ];
1430 
1431     t = JXG.createText(board, par, attr);
1432     t.type = Type.OBJECT_TYPE_HTMLSLIDER;
1433 
1434     t.rendNodeForm = t.rendNode.childNodes[0];
1435 
1436     t.rendNodeRange = t.rendNodeForm.childNodes[0];
1437     t.rendNodeRange.min = parents[1][0];
1438     t.rendNodeRange.max = parents[1][2];
1439     t.rendNodeRange.step = attr.step;
1440     t.rendNodeRange.value = parents[1][1];
1441 
1442     t.rendNodeLabel = t.rendNodeForm.childNodes[1];
1443     t.rendNodeLabel.id = t.rendNode.id + "_label";
1444 
1445     if (attr.withlabel) {
1446         t.rendNodeLabel.innerHTML = t.name + "=";
1447     }
1448 
1449     t.rendNodeOut = t.rendNodeForm.childNodes[2];
1450     t.rendNodeOut.value = parents[1][1];
1451 
1452     try {
1453         t.rendNodeForm.id = t.rendNode.id + "_form";
1454         t.rendNodeRange.id = t.rendNode.id + "_range";
1455         t.rendNodeOut.id = t.rendNode.id + "_out";
1456     } catch (e) {
1457         JXG.debug(e);
1458     }
1459 
1460     t.rendNodeRange.style.width = attr.widthrange + "px";
1461     t.rendNodeRange.style.verticalAlign = "middle";
1462     t.rendNodeOut.style.width = attr.widthout + "px";
1463 
1464     t._val = parents[1][1];
1465 
1466     if (JXG.supportsVML()) {
1467         /*
1468          * OnChange event is used for IE browsers
1469          * The range element is supported since IE10
1470          */
1471         Env.addEvent(t.rendNodeForm, "change", priv.HTMLSliderInputEventHandler, t);
1472     } else {
1473         /*
1474          * OnInput event is used for non-IE browsers
1475          */
1476         Env.addEvent(t.rendNodeForm, "input", priv.HTMLSliderInputEventHandler, t);
1477     }
1478 
1479     t.Value = function () {
1480         return this._val;
1481     };
1482 
1483     return t;
1484 };
1485 
1486 JXG.registerElement("htmlslider", JXG.createHTMLSlider);
1487 
1488 export default JXG.Text;
1489 // export default {
1490 //     Text: JXG.Text,
1491 //     createText: JXG.createText,
1492 //     createHTMLSlider: JXG.createHTMLSlider
1493 // };
1494