1 /*
  2     Copyright 2008-2015
  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 <http://www.gnu.org/licenses/>
 29     and <http://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 
 33 /*global JXG: true, define: true, window: true*/
 34 /*jslint nomen: true, plusplus: true*/
 35 
 36 /* depends:
 37  jxg
 38  base/constants
 39  base/coords
 40  base/element
 41  parser/geonext
 42  math/statistics
 43  utils/env
 44  utils/type
 45  */
 46 
 47 /**
 48  * @fileoverview In this file the Text element is defined.
 49  */
 50 
 51 define([
 52     'jxg', 'base/constants', 'base/coords', 'base/element', 'parser/geonext', 'math/statistics',
 53     'utils/env', 'utils/type', 'math/math', 'base/coordselement'
 54 ], function (JXG, Const, Coords, GeometryElement, GeonextParser, Statistics, Env, Type, Mat, CoordsElement) {
 55 
 56     "use strict";
 57 
 58     var priv = {
 59             HTMLSliderInputEventHandler: function () {
 60                 this._val = parseFloat(this.rendNodeRange.value);
 61                 this.rendNodeOut.value = this.rendNodeRange.value;
 62                 this.board.update();
 63             }
 64         };
 65 
 66     /**
 67      * Construct and handle texts.
 68      * 
 69      * The coordinates can be relative to the coordinates of an element 
 70      * given in {@link JXG.Options#text.anchor}.
 71      * 
 72      * MathJax, HTML and GEONExT syntax can be handled.
 73      * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with
 74      * type {@link Text} instead.
 75      * @augments JXG.GeometryElement
 76      * @augments JXG.CoordsElement
 77      * @param {string|JXG.Board} board The board the new text is drawn on.
 78      * @param {Array} coordinates An array with the user coordinates of the text.
 79      * @param {Object} attributes An object containing visual properties and optional a name and a id.
 80      * @param {string|function} content A string or a function returning a string.
 81      *
 82      */
 83     JXG.Text = function (board, coords, attributes, content) {
 84         this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT);
 85 
 86         this.element = this.board.select(attributes.anchor);
 87         this.coordsConstructor(coords, this.visProp.islabel);
 88 
 89         this.content = '';
 90         this.plaintext = '';
 91         this.plaintextOld = null;
 92         this.orgText = '';
 93 
 94         this.needsSizeUpdate = false;
 95         this.hiddenByParent = false;
 96 
 97         this.size = [1.0, 1.0];
 98         this.id = this.board.setId(this, 'T');
 99 
100         // Set text before drawing
101         this._setUpdateText(content);
102         this.updateText();
103 
104         this.board.renderer.drawText(this);
105         this.board.finalizeAdding(this);
106 
107         if (typeof this.content === 'string') {
108             this.notifyParents(this.content);
109         }
110         this.elType = 'text';
111 
112         this.methodMap = Type.deepCopy(this.methodMap, {
113             setText: 'setTextJessieCode',
114             // free: 'free',
115             move: 'setCoords'
116         });
117     };
118 
119     JXG.Text.prototype = new GeometryElement();
120     Type.copyPrototypeMethods(JXG.Text, CoordsElement, 'coordsConstructor');
121 
122     JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
123         /**
124          * @private
125          * Test if the the screen coordinates (x,y) are in a small stripe
126          * at the left side or at the right side of the text.
127          * Sensitivity is set in this.board.options.precision.hasPoint.
128          * If dragarea is set to 'all' (default), tests if the the screen 
129         * coordinates (x,y) are in within the text boundary.
130          * @param {Number} x
131          * @param {Number} y
132          * @return {Boolean}
133          */
134         hasPoint: function (x, y) {
135             var lft, rt, top, bot,
136                 r = this.board.options.precision.hasPoint;
137 
138             if (this.transformations.length > 0) {
139                 /**
140                  * Transform the mouse/touch coordinates
141                  * back to the original position of the text.
142                  */
143                 lft = Mat.matVecMult(Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)), [1, x, y]);
144                 x = lft[1];
145                 y = lft[2];
146             }
147 
148             if (this.visProp.anchorx === 'right') {
149                 lft = this.coords.scrCoords[1] - this.size[0];
150             } else if (this.visProp.anchorx === 'middle') {
151                 lft = this.coords.scrCoords[1] - 0.5 * this.size[0];
152             } else {
153                 lft = this.coords.scrCoords[1];
154             }
155             rt = lft + this.size[0];
156 
157             if (this.visProp.anchory === 'top') {
158                 bot = this.coords.scrCoords[2] + this.size[1];
159             } else if (this.visProp.anchory === 'middle') {
160                 bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
161             } else {
162                 bot = this.coords.scrCoords[2];
163             }
164             top = bot - this.size[1];
165 
166             if (this.visProp.dragarea === 'all') {
167                 return x >= lft - r && x < rt + r && y >= top - r  && y <= bot + r;
168             }
169 
170             return (y >= top - r && y <= bot + r) &&
171                 ((x >= lft - r  && x <= lft + 2 * r) ||
172                 (x >= rt - 2 * r && x <= rt + r));
173         },
174 
175         /**
176          * This sets the updateText function of this element that depending on the type of text content passed.
177          * Used by {@link JXG.Text#_setText} and {@link JXG.Text} constructor.
178          * @param {String|Function|Number} text
179          * @private
180          */
181         _setUpdateText: function (text) {
182             var updateText;
183 
184             this.orgText = text;
185             if (typeof text === 'function') {
186                 this.updateText = function () {
187                     if (this.visProp.parse && !this.visProp.usemathjax) {
188                         this.plaintext = this.replaceSub(this.replaceSup(this.convertGeonext2CSS(text())));
189                     } else {
190                         this.plaintext = text();
191                     }
192                 };
193             } else if (Type.isString(text) && !this.visProp.parse) {
194                 this.updateText = function () {
195                     this.plaintext = text;
196                 };
197             } else {
198                 if (Type.isNumber(text)) {
199                     this.content = text.toFixed(this.visProp.digits);
200                 } else {
201                     if (this.visProp.useasciimathml) {
202                         // Convert via ASCIIMathML
203                         this.content = "'`" + text + "`'";
204                     } else if (this.visProp.usemathjax) {
205                         this.content = "'" + text + "'";
206                     } else {
207                         // Converts GEONExT syntax into JavaScript string
208                         this.content = this.generateTerm(text);
209                     }
210                 }
211                 updateText = this.board.jc.snippet(this.content, true, '', false);
212                 this.updateText = function () {
213                     this.plaintext = updateText();
214                 };
215             }
216         },
217 
218         /**
219          * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
220          * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
221          * @param {String|Function|Number} text
222          * @return {JXG.Text}
223          * @private
224          */
225         _setText: function (text) {
226             this._setUpdateText(text);
227 
228             // First evaluation of the string.
229             // We need this for display='internal' and Canvas
230             this.updateText();
231             this.prepareUpdate().update().updateRenderer();
232 
233             // We do not call updateSize for the infobox to speed up rendering
234             if (!this.board.infobox || this.id !== this.board.infobox.id) {
235                 this.updateSize();    // updateSize() is called at least once.
236             }
237 
238             return this;
239         },
240 
241         /**
242          * Defines new content but converts < and > to HTML entities before updating the DOM.
243          * @param {String|function} text
244          */
245         setTextJessieCode: function (text) {
246             var s;
247 
248             this.visProp.castext = text;
249 
250             if (typeof text === 'function') {
251                 s = function () {
252                     return Type.sanitizeHTML(text());
253                 };
254             } else {
255                 if (Type.isNumber(text)) {
256                     s = text;
257                 } else {
258                     s = Type.sanitizeHTML(text);
259                 }
260             }
261 
262             return this._setText(s);
263         },
264 
265         /**
266          * Defines new content.
267          * @param {String|function} text
268          * @return {JXG.Text} Reference to the text object.
269          */
270         setText: function (text) {
271             return this._setText(text);
272         },
273 
274         /**
275          * Recompute the width and the height of the text box.
276          * Update array this.size with pixel values.
277          * The result may differ from browser to browser
278          * by some pixels.
279          * In canvas an old IEs we use a very crude estimation of the dimensions of
280          * the textbox.
281          * In JSXGraph this.size is necessary for applying rotations in IE and
282          * for aligning text.
283          */
284         updateSize: function () {
285             var tmp, s, that;
286 
287             if (!Env.isBrowser || this.board.renderer.type === 'no') {
288                 return this;
289             }
290 
291             /**
292              * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode.
293              */
294             if (this.visProp.display === 'html' || this.board.renderer.type === 'vml') {
295                 if (JXG.exists(this.rendNode.offsetWidth)) {
296                     s = [this.rendNode.offsetWidth, this.rendNode.offsetHeight];
297                     if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight
298                         that = this;
299                         window.setTimeout(function () {
300                             that.size = [that.rendNode.offsetWidth, that.rendNode.offsetHeight];
301                         }, 0);
302                     } else {
303                         this.size = s;
304                     }
305                 } else {
306                     this.size = this.crudeSizeEstimate();
307                 }
308             } else if (this.visProp.display === 'internal') {
309                 if (this.board.renderer.type === 'svg') {
310                     try {
311                         tmp = this.rendNode.getBBox();
312                         this.size = [tmp.width, tmp.height];
313                     } catch (e) {}
314                 } else if (this.board.renderer.type === 'canvas') {
315                     this.size = this.crudeSizeEstimate();
316                 }
317             }
318 
319             return this;
320         },
321 
322         /**
323          * A very crude estimation of the dimensions of the textbox in case nothing else is available.
324          * @return {Array}
325          */
326         crudeSizeEstimate: function () {
327             return [parseFloat(this.visProp.fontsize) * this.plaintext.length * 0.45, parseFloat(this.visProp.fontsize) * 0.9];
328         },
329 
330         /**
331          * Decode unicode entities into characters.
332          * @param {String} string
333          * @returns {String}
334          */
335         utf8_decode : function (string) {
336             return string.replace(/&#x(\w+);/g, function (m, p1) {
337                 return String.fromCharCode(parseInt(p1, 16));
338             });
339         },
340 
341         /**
342          * Replace _{} by <sub>
343          * @param {String} te String containing _{}.
344          * @returns {String} Given string with _{} replaced by <sub>.
345          */
346         replaceSub: function (te) {
347             if (!te.indexOf) {
348                 return te;
349             }
350 
351             var j,
352                 i = te.indexOf('_{');
353 
354             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
355             // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway.
356             /*jslint regexp: true*/
357 
358             while (i >= 0) {
359                 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>');
360                 j = te.substr(i).indexOf('}');
361                 if (j >= 0) {
362                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>');
363                 }
364                 i = te.indexOf('_{');
365             }
366 
367             i = te.indexOf('_');
368             while (i >= 0) {
369                 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>');
370                 i = te.indexOf('_');
371             }
372 
373             return te;
374         },
375 
376         /**
377          * Replace ^{} by <sup>
378          * @param {String} te String containing ^{}.
379          * @returns {String} Given string with ^{} replaced by <sup>.
380          */
381         replaceSup: function (te) {
382             if (!te.indexOf) {
383                 return te;
384             }
385 
386             var j,
387                 i = te.indexOf('^{');
388 
389             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
390             // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway.
391             /*jslint regexp: true*/
392 
393             while (i >= 0) {
394                 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>');
395                 j = te.substr(i).indexOf('}');
396                 if (j >= 0) {
397                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>');
398                 }
399                 i = te.indexOf('^{');
400             }
401 
402             i = te.indexOf('^');
403             while (i >= 0) {
404                 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>');
405                 i = te.indexOf('^');
406             }
407 
408             return te;
409         },
410 
411         /**
412          * Return the width of the text element.
413          * @return {Array} [width, height] in pixel
414          */
415         getSize: function () {
416             return this.size;
417         },
418 
419         /**
420          * Move the text to new coordinates.
421          * @param {number} x
422          * @param {number} y
423          * @return {object} reference to the text object.
424          */
425         setCoords: function (x, y) {
426             var coordsAnchor, dx, dy;
427             if (Type.isArray(x) && x.length > 1) {
428                 y = x[1];
429                 x = x[0];
430             }
431 
432             if (this.visProp.islabel && Type.exists(this.element)) {
433                 coordsAnchor = this.element.getLabelAnchor();
434                 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX;
435                 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY;
436 
437                 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]);
438             } else {
439                 /*
440                 this.X = function () {
441                     return x;
442                 };
443 
444                 this.Y = function () {
445                     return y;
446                 };
447                 */
448                 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]);
449             }
450 
451             // this should be a local update, otherwise there might be problems
452             // with the tick update routine resulting in orphaned tick labels
453             this.prepareUpdate().update().updateRenderer();
454 
455             return this;
456         },
457 
458         /**
459          * Evaluates the text.
460          * Then, the update function of the renderer
461          * is called.
462          */
463         update: function (fromParent) {
464             if (!this.needsUpdate) {
465                 return this;
466             }
467 
468             this.updateCoords(fromParent);
469             this.updateText();
470 
471             if (this.visProp.display === 'internal') {
472                 this.plaintext = this.utf8_decode(this.plaintext);
473             }
474 
475             this.checkForSizeUpdate();
476             if (this.needsSizeUpdate) {
477                 this.updateSize();
478             }
479 
480             return this;
481         },
482 
483         /**
484          * Used to save updateSize() calls.
485          * Called in JXG.Text.update
486          * That means this.update() has been called.
487          * More tests are in JXG.Renderer.updateTextStyle. The latter tests
488          * are one update off. But this should pose not too many problems, since
489          * it affects fontSize and cssClass changes.
490          *
491          * @private
492          */
493         checkForSizeUpdate: function () {
494             if (this.board.infobox && this.id === this.board.infobox.id) {
495                 this.needsSizeUpdate = false;
496             } else {
497                 // For some magic reason it is more efficient on the iPad to
498                 // call updateSize() for EVERY text element EVERY time.
499                 this.needsSizeUpdate = (this.plaintextOld !== this.plaintext);
500 
501                 if (this.needsSizeUpdate) {
502                     this.plaintextOld = this.plaintext;
503                 }
504             }
505 
506         },
507 
508         /**
509          * The update function of the renderert
510          * is called.
511          * @private
512          */
513         updateRenderer: function () {
514             return this.updateRendererGeneric('updateText');
515         },
516 
517         /**
518          * Converts the GEONExT syntax of the <value> terms into JavaScript.
519          * Also, all Objects whose name appears in the term are searched and
520          * the text is added as child to these objects.
521          * @private
522          * @see JXG.GeonextParser.geonext2JS.
523          */
524         generateTerm: function (contentStr) {
525             var res, term, i, j,
526                 plaintext = '""';
527 
528             // revert possible jc replacement
529             contentStr = contentStr || '';
530             contentStr = contentStr.replace(/\r/g, '');
531             contentStr = contentStr.replace(/\n/g, '');
532             contentStr = contentStr.replace(/"/g, '\'');
533             contentStr = contentStr.replace(/'/g, "\\'");
534 
535             contentStr = contentStr.replace(/&arc;/g, '∠');
536             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
537             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
538             contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√');
539 
540             contentStr = contentStr.replace(/<value>/g, '<value>');
541             contentStr = contentStr.replace(/<\/value>/g, '</value>');
542 
543             // Convert GEONExT syntax into  JavaScript syntax
544             i = contentStr.indexOf('<value>');
545             j = contentStr.indexOf('</value>');
546             if (i >= 0) {
547                 while (i >= 0) {
548                     plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"';
549                     term = contentStr.slice(i + 7, j);
550                     res = GeonextParser.geonext2JS(term, this.board);
551                     res = res.replace(/\\"/g, "'");
552                     res = res.replace(/\\'/g, "'");
553 
554                     // GEONExT-Hack: apply rounding once only.
555                     if (res.indexOf('toFixed') < 0) {
556                         // output of a value tag
557                         if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) {
558                             // may also be a string
559                             plaintext += '+(' + res + ').toFixed(' + (this.visProp.digits) + ')';
560                         } else {
561                             plaintext += '+(' + res + ')';
562                         }
563                     } else {
564                         plaintext += '+(' + res + ')';
565                     }
566 
567                     contentStr = contentStr.slice(j + 8);
568                     i = contentStr.indexOf('<value>');
569                     j = contentStr.indexOf('</value>');
570                 }
571             }
572 
573             plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"';
574             plaintext = this.convertGeonext2CSS(plaintext);
575 
576             // This should replace &pi; by π
577             plaintext = plaintext.replace(/&/g, '&');
578             plaintext = plaintext.replace(/"/g, "'");
579 
580             return plaintext;
581         },
582 
583         /**
584          * Converts the GEONExT tags <overline> and <arrow> to
585          * HTML span tags with proper CSS formating.
586          * @private
587          * @see JXG.Text.generateTerm @see JXG.Text._setText
588          */
589         convertGeonext2CSS: function (s) {
590             if (typeof s === 'string') {
591                 s = s.replace(/<overline>/g, '<span style=text-decoration:overline>');
592                 s = s.replace(/<overline>/g, '<span style=text-decoration:overline>');
593                 s = s.replace(/<\/overline>/g, '</span>');
594                 s = s.replace(/<\/overline>/g, '</span>');
595                 s = s.replace(/<arrow>/g, '<span style=text-decoration:overline>');
596                 s = s.replace(/<arrow>/g, '<span style=text-decoration:overline>');
597                 s = s.replace(/<\/arrow>/g, '</span>');
598                 s = s.replace(/<\/arrow>/g, '</span>');
599             }
600 
601             return s;
602         },
603 
604         /**
605          * Finds dependencies in a given term and notifies the parents by adding the
606          * dependent object to the found objects child elements.
607          * @param {String} content String containing dependencies for the given object.
608          * @private
609          */
610         notifyParents: function (content) {
611             var search,
612                 res = null;
613 
614             // revert possible jc replacement
615             content = content.replace(/<value>/g, '<value>');
616             content = content.replace(/<\/value>/g, '</value>');
617 
618             do {
619                 search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/;
620                 res = search.exec(content);
621 
622                 if (res !== null) {
623                     GeonextParser.findDependencies(this, res[1], this.board);
624                     content = content.substr(res.index);
625                     content = content.replace(search, '');
626                 }
627             } while (res !== null);
628 
629             return this;
630         },
631 
632         bounds: function () {
633             var c = this.coords.usrCoords;
634 
635             return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2] + this.size[1], c[1] + this.size[0], c[2]];
636         }
637     });
638 
639     /**
640      * @class Construct and handle texts.
641      * 
642      * The coordinates can be relative to the coordinates of an element 
643      * given in {@link JXG.Options#text.anchor}.
644      * 
645      * MathJaX, HTML and GEONExT syntax can be handled.
646      * @pseudo
647      * @description
648      * @name Text
649      * @augments JXG.Text
650      * @constructor
651      * @type JXG.Text
652      *
653      * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements.
654      *                     <p>
655      *   Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T
656      *   constraint, or a function which takes no parameter and returns a number. Every parent element determines one coordinate. If a coordinate is
657      *   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
658      *   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
659      *   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
660      *   parent elements are given they will be interpreted as homogeneous coordinates.
661      *                     <p>
662      *                     The text to display may be given as string or as function returning a string.
663      *
664      * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
665      * the text. In this case it is also possible to use ASCIIMathML. Incase of 'internal', a SVG or VML text element is used to display the text.
666      * @see JXG.Text
667      * @example
668      * // Create a fixed text at position [0,1].
669      *   var t1 = board.create('text',[0,1,"Hello World"]);
670      * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
671      * <script type="text/javascript">
672      *   var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
673      *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
674      * </script><pre>
675      * @example
676      * // Create a variable text at a variable position.
677      *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
678      *   var graph = board.create('text',
679      *                        [function(x){ return s.Value();}, 1,
680      *                         function(){return "The value of s is"+s.Value().toFixed(2);}
681      *                        ]
682      *                     );
683      * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
684      * <script type="text/javascript">
685      *   var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
686      *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
687      *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]);
688      * </script><pre>
689      */
690     JXG.createText = function (board, parents, attributes) {
691         var t,
692             attr = Type.copyAttributes(attributes, board.options, 'text'),
693             coords = parents.slice(0, -1),
694             content = parents[parents.length - 1];
695 
696         // downwards compatibility
697         attr.anchor = attr.parent || attr.anchor;
698         t = CoordsElement.create(JXG.Text, board, coords, attr, content);
699 
700         if (!t) {
701             throw new Error("JSXGraph: Can't create text with parent types '" +
702                     (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
703                     "\nPossible parent types: [x,y], [z,x,y], [element,transformation]");
704         }
705 
706         if (Type.evaluate(attr.rotate) !== 0 && attr.display === 'internal') {
707             t.addRotation(Type.evaluate(attr.rotate));
708         }
709 
710         return t;
711     };
712 
713     JXG.registerElement('text', JXG.createText);
714 
715     /**
716      * [[x,y], [w px, h px], [range]
717      */
718     JXG.createHTMLSlider = function (board, parents, attributes) {
719         var t, par,
720             attr = Type.copyAttributes(attributes, board.options, 'htmlslider');
721 
722         if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) {
723             throw new Error("JSXGraph: Can't create htmlslider with parent types '" +
724                 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
725                 "\nPossible parents are: [[x,y], [min, start, max]]");
726         }
727 
728         // backwards compatibility
729         attr.anchor = attr.parent || attr.anchor;
730         attr.fixed = attr.fixed || true;
731 
732         par = [parents[0][0], parents[0][1],
733             '<form style="display:inline">' +
734             '<input type="range" /><span></span><input type="text" />' +
735             '</form>'];
736 
737         t = JXG.createText(board, par, attr);
738         t.type = Type.OBJECT_TYPE_HTMLSLIDER;
739 
740         t.rendNodeForm = t.rendNode.childNodes[0];
741         t.rendNodeForm.id = t.rendNode.id + '_form';
742 
743         t.rendNodeRange = t.rendNodeForm.childNodes[0];
744         t.rendNodeRange.id = t.rendNode.id + '_range';
745         t.rendNodeRange.min = parents[1][0];
746         t.rendNodeRange.max = parents[1][2];
747         t.rendNodeRange.step = attr.step;
748         t.rendNodeRange.value = parents[1][1];
749 
750         t.rendNodeLabel = t.rendNodeForm.childNodes[1];
751         t.rendNodeLabel.id = t.rendNode.id + '_label';
752 
753         if (attr.withlabel) {
754             t.rendNodeLabel.innerHTML = t.name + '=';
755         }
756 
757         t.rendNodeOut = t.rendNodeForm.childNodes[2];
758         t.rendNodeOut.id = t.rendNode.id + '_out';
759         t.rendNodeOut.value = parents[1][1];
760 
761         t.rendNodeRange.style.width = attr.widthrange + 'px';
762         t.rendNodeRange.style.verticalAlign = 'middle';
763         t.rendNodeOut.style.width = attr.widthout + 'px';
764 
765         t._val = parents[1][1];
766 
767         if (JXG.supportsVML()) {
768             /*
769             * OnChange event is used for IE browsers
770             * The range element is supported since IE10
771             */
772             Env.addEvent(t.rendNodeForm, 'change', priv.HTMLSliderInputEventHandler, t);
773         } else {
774             /*
775             * OnInput event is used for non-IE browsers
776             */
777             Env.addEvent(t.rendNodeForm, 'input', priv.HTMLSliderInputEventHandler, t);
778         }
779 
780         t.Value = function () {
781             return this._val;
782         };
783 
784         return t;
785     };
786 
787     JXG.registerElement('htmlslider', JXG.createHTMLSlider);
788 
789     return {
790         Text: JXG.Text,
791         createText: JXG.createText,
792         createHTMLSlider: JXG.createHTMLSlider
793     };
794 });
795