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