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