1 /*
  2     Copyright 2008-2013
  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', 'utils/env', 'utils/type'
 53 ], function (JXG, Const, Coords, GeometryElement, GeonextParser, Statistics, Env, Type) {
 54 
 55     "use strict";
 56 
 57     /**
 58      * Construct and handle texts.
 59      * @class Text: On creation the GEONExT syntax
 60      * of <value>-terms
 61      * are converted into JavaScript syntax.
 62      * The coordinates can be relative to the coordinates of an element "element".
 63      * @constructor
 64      * @return A new geometry element Text
 65      */
 66     JXG.Text = function (board, content, coords, attributes) {
 67         this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_OTHER);
 68 
 69         var i, anchor;
 70 
 71         this.content = '';
 72         this.plaintext = '';
 73 
 74         this.isDraggable = false;
 75         this.needsSizeUpdate = false;
 76         this.element = this.board.select(attributes.anchor);
 77 
 78         this.hiddenByParent = false;
 79 
 80         if (this.element) {
 81             if (this.visProp.islabel) {
 82                 this.relativeCoords = new Coords(Const.COORDS_BY_SCREEN, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 83             } else {
 84                 this.relativeCoords = new Coords(Const.COORDS_BY_USER, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 85             }
 86             this.element.addChild(this);
 87 
 88             this.X = function () {
 89                 var sx, coords, anchor;
 90 
 91                 if (this.visProp.islabel) {
 92                     sx =  parseFloat(this.visProp.offset[0]);
 93                     anchor = this.element.getLabelAnchor();
 94                     coords = new Coords(Const.COORDS_BY_SCREEN, [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board);
 95 
 96                     return coords.usrCoords[1];
 97                 }
 98 
 99                 anchor = this.element.getTextAnchor();
100                 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1];
101             };
102 
103             this.Y = function () {
104                 var sy, coords, anchor;
105 
106                 if (this.visProp.islabel) {
107                     sy = -parseFloat(this.visProp.offset[1]);
108                     anchor = this.element.getLabelAnchor();
109                     coords = new Coords(Const.COORDS_BY_SCREEN, [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board);
110 
111                     return coords.usrCoords[2];
112                 }
113 
114                 anchor = this.element.getTextAnchor();
115                 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2];
116             };
117 
118             this.coords = new Coords(Const.COORDS_BY_SCREEN, [0, 0], this.board);
119             this.isDraggable = true;
120         } else {
121             if (Type.isNumber(coords[0]) && Type.isNumber(coords[1])) {
122                 this.isDraggable = true;
123             }
124             this.X = Type.createFunction(coords[0], this.board, null, true);
125             this.Y = Type.createFunction(coords[1], this.board, null, true);
126 
127             this.coords = new Coords(Const.COORDS_BY_USER, [this.X(), this.Y()], this.board);
128         }
129 
130         this.Z = Type.createFunction(1, this.board, '');
131         this.size = [1.0, 1.0];
132         this.id = this.board.setId(this, 'T');
133         this.board.renderer.drawText(this);
134 
135         this.setText(content);
136         this.updateSize();
137 
138         if (!this.visProp.visible) {
139             this.board.renderer.hide(this);
140         }
141 
142         if (typeof this.content === 'string') {
143             this.notifyParents(this.content);
144         }
145 
146         this.elType = 'text';
147 
148         this.methodMap = Type.deepCopy(this.methodMap, {
149             setText: 'setTextJessieCode',
150             free: 'free',
151             move: 'setCoords'
152         });
153 
154         return this;
155     };
156 
157     JXG.Text.prototype = new GeometryElement();
158 
159     JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
160         /**
161          * @private
162          * Test if the the screen coordinates (x,y) are in a small stripe
163          * at the left side or at the right side of the text.
164          * Sensitivity is set in this.board.options.precision.hasPoint.
165          * @param {Number} x
166          * @param {Number} y
167          * @return {Boolean}
168          */
169         hasPoint: function (x, y) {
170             var lft, rt, top, bot,
171                 r = this.board.options.precision.hasPoint;
172 
173             if (this.visProp.anchorx === 'right') {
174                 lft = this.coords.scrCoords[1] - this.size[0];
175             } else if (this.visProp.anchorx === 'middle') {
176                 lft = this.coords.scrCoords[1] - 0.5 * this.size[0];
177             } else {
178                 lft = this.coords.scrCoords[1];
179             }
180             rt = lft + this.size[0];
181 
182             if (this.visProp.anchory === 'top') {
183                 bot = this.coords.scrCoords[2] + this.size[1];
184             } else if (this.visProp.anchory === 'middle') {
185                 bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
186             } else {
187                 bot = this.coords.scrCoords[2];
188             }
189             top = bot - this.size[1];
190 
191             if (this.visProp.dragarea === 'all') {
192                 return x >= lft - r && x < rt + r && y >= top - r  && y <= bot + r;
193             }
194 
195             return (y >= top - r && y <= bot + r) &&
196                 ((x >= lft - r  && x <= lft + 2 * r) ||
197                 (x >= rt - 2 * r && x <= rt + r));
198         },
199 
200         /**
201          * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
202          * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
203          * @param {String|Function|Number} text
204          * @return {JXG.Text}
205          * @private
206          */
207         _setText: function (text) {
208             var updateText;
209 
210             this.needsSizeUpdate = false;
211 
212             if (typeof text === 'function') {
213                 this.updateText = function () {
214                     this.plaintext = text();
215                 };
216                 this.needsSizeUpdate = true;
217             } else if (Type.isString(text) && !this.visProp.parse) {
218                 this.updateText = function () {
219                     this.plaintext = text;
220                 };
221                 this.needsSizeUpdate = false;  // true;
222             } else {
223                 if (Type.isNumber(text)) {
224                     this.content = text.toFixed(this.visProp.digits);
225                 } else {
226                     if (this.visProp.useasciimathml) {
227                         // Convert via ASCIIMathML
228                         this.content = "'`" + text + "`'";
229                         this.needsSizeUpdate = true;
230                     } else {
231                         // Converts GEONExT syntax into JavaScript string
232                         this.content = this.generateTerm(text);
233                     }
234                 }
235                 updateText = this.board.jc.snippet(this.content, true, '', false);
236                 this.updateText = function () {
237                     this.plaintext = updateText();
238                 };
239             }
240 
241             // First evaluation of the string.
242             // We need this for display='internal' and Canvas
243             this.updateText();
244 
245             this.prepareUpdate().update().updateRenderer();
246 
247             // call updateSize() at least once.
248             if (this.needsSizeUpdate) {
249                 this.updateSize();
250             }
251 
252             return this;
253         },
254 
255         /**
256          * Defines new content but converts < and > to HTML entities before updating the DOM.
257          * @param {String|function} text
258          */
259         setTextJessieCode: function (text) {
260             var s;
261 
262             this.visProp.castext = text;
263 
264             if (typeof text === 'function') {
265                 s = function () {
266                     return Type.sanitizeHTML(text());
267                 };
268             } else {
269                 if (Type.isNumber(text)) {
270                     s = text;
271                 } else {
272                     s = Type.sanitizeHTML(text);
273 
274                 }
275             }
276 
277             return this._setText(s);
278         },
279 
280         /**
281          * Defines new content.
282          * @param {String|function} text
283          * @return {JXG.Text} Reference to the text object.
284          */
285         setText: function (text) {
286             return this._setText(text);
287         },
288 
289         /**
290          * Recompute the width and the height of the text box.
291          * Update array this.size with pixel values.
292          * The result may differ from browser to browser
293          * by some pixels.
294          * In IE and canvas we use a very crude estimation of the dimensions of
295          * the textbox.
296          * In JSXGraph this.size is necessary for applying rotations in IE and
297          * for aligning text.
298          */
299         updateSize: function () {
300             var tmp, s, that;
301 
302             if (!Env.isBrowser) {
303                 return this;
304             }
305 
306             if (this.visProp.display === 'html' && this.board.renderer.type !== 'vml' && this.board.renderer.type !== 'no') {
307                 s = [this.rendNode.offsetWidth, this.rendNode.offsetHeight];
308                 if (s[0] === 0 && s[1] === 0) {
309                     // Some browsers need some time to set offsetWidth and offsetHeight
310                     that = this;
311                     window.setTimeout(function () {
312                         that.size = [that.rendNode.offsetWidth, that.rendNode.offsetHeight];
313                     }, 0);
314                 } else {
315                     this.size = s;
316                 }
317 
318             } else if (this.visProp.display === 'internal' && this.board.renderer.type === 'svg') {
319                 try {
320                     tmp = this.rendNode.getBBox();
321                     this.size = [tmp.width, tmp.height];
322                 } catch (e) {
323                 }
324             } else if (this.board.renderer.type === 'vml' || (this.visProp.display === 'internal' && this.board.renderer.type === 'canvas')) {
325                 // Here comes a very crude estimation of the dimensions of the textbox.
326                 this.size = [parseFloat(this.visProp.fontsize) * this.plaintext.length * 0.45, parseFloat(this.visProp.fontsize) * 0.9];
327             }
328 
329             return this;
330         },
331 
332         /**
333          * Decode unicode entities into characters.
334          * @param {String} string
335          * @returns {String}
336          */
337         utf8_decode : function (string) {
338             return string.replace(/&#x(\w+);/g, function (m, p1) {
339                 return String.fromCharCode(parseInt(p1, 16));
340             });
341         },
342 
343         /**
344          * Replace _{} by <sub>
345          * @param {String} te String containing _{}.
346          * @returns {String} Given string with _{} replaced by <sub>.
347          */
348         replaceSub: function (te) {
349             if (!te.indexOf) {
350                 return te;
351             }
352 
353             var j,
354                 i = te.indexOf('_{');
355 
356             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
357             // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway.
358             /*jslint regexp: true*/
359 
360             while (i >= 0) {
361                 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>');
362                 j = te.substr(i).indexOf('}');
363                 if (j >= 0) {
364                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>');
365                 }
366                 i = te.indexOf('_{');
367             }
368 
369             i = te.indexOf('_');
370             while (i >= 0) {
371                 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>');
372                 i = te.indexOf('_');
373             }
374 
375             return te;
376         },
377 
378         /**
379          * Replace ^{} by <sup>
380          * @param {String} te String containing ^{}.
381          * @returns {String} Given string with ^{} replaced by <sup>.
382          */
383         replaceSup: function (te) {
384             if (!te.indexOf) {
385                 return te;
386             }
387 
388             var j,
389                 i = te.indexOf('^{');
390 
391             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
392             // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway.
393             /*jslint regexp: true*/
394 
395             while (i >= 0) {
396                 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>');
397                 j = te.substr(i).indexOf('}');
398                 if (j >= 0) {
399                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>');
400                 }
401                 i = te.indexOf('^{');
402             }
403 
404             i = te.indexOf('^');
405             while (i >= 0) {
406                 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>');
407                 i = te.indexOf('^');
408             }
409 
410             return te;
411         },
412 
413         /**
414          * Return the width of the text element.
415          * @return {Array} [width, height] in pixel
416          */
417         getSize: function () {
418             return this.size;
419         },
420 
421         /**
422          * Move the text to new coordinates.
423          * @param {number} x
424          * @param {number} y
425          * @return {object} reference to the text object.
426          */
427         setCoords: function (x, y) {
428             if (Type.isArray(x) && x.length > 1) {
429                 y = x[1];
430                 x = x[0];
431             }
432 
433             this.X = function () {
434                 return x;
435             };
436 
437             this.Y = function () {
438                 return y;
439             };
440 
441             this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]);
442 
443             // this should be a local update, otherwise there might be problems
444             // with the tick update routine resulting in orphaned tick labels
445             this.prepareUpdate().update().updateRenderer();
446 
447             return this;
448         },
449 
450         free: function () {
451             this.X = Type.createFunction(this.X(), this.board, '');
452             this.Y = Type.createFunction(this.Y(), this.board, '');
453 
454             this.isDraggable = true;
455         },
456 
457         /**
458          * Evaluates the text.
459          * Then, the update function of the renderer
460          * is called.
461          */
462         update: function () {
463             if (this.needsUpdate) {
464                 if (!this.visProp.frozen) {
465                     this.updateCoords();
466                 }
467                 this.updateText();
468 
469                 if (this.visProp.display === 'internal') {
470                     this.plaintext = this.utf8_decode(this.plaintext);
471                 }
472 
473                 if (this.needsSizeUpdate) {
474                     this.updateSize();
475                 }
476                 this.updateTransform();
477 
478             }
479 
480             return this;
481         },
482 
483         /**
484          * Updates the coordinates of the text element.
485          */
486         updateCoords: function () {
487             this.coords.setCoordinates(Const.COORDS_BY_USER, [this.X(), this.Y()]);
488         },
489 
490         /**
491          * The update function of the renderert
492          * is called.
493          * @private
494          */
495         updateRenderer: function () {
496             if (this.needsUpdate) {
497                 this.board.renderer.updateText(this);
498                 this.needsUpdate = false;
499             }
500             return this;
501         },
502 
503         updateTransform: function () {
504             var i;
505 
506             if (this.transformations.length === 0) {
507                 return this;
508             }
509 
510             for (i = 0; i < this.transformations.length; i++) {
511                 this.transformations[i].update();
512             }
513 
514             return this;
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             contentStr = contentStr.replace(/&arc;/g, '∠');
535             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
536             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
537             contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√');
538             contentStr = contentStr.replace(/<value>/g, '<value>');
539             contentStr = contentStr.replace(/<\/value>/g, '</value>');
540 
541             // Convert GEONExT syntax into  JavaScript syntax
542             i = contentStr.indexOf('<value>');
543             j = contentStr.indexOf('</value>');
544             if (i >= 0) {
545                 this.needsSizeUpdate = true;
546                 while (i >= 0) {
547                     plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"';
548                     term = contentStr.slice(i + 7, j);
549                     res = GeonextParser.geonext2JS(term, this.board);
550                     res = res.replace(/\\"/g, "'");
551                     res = res.replace(/\\'/g, "'");
552 
553                     // GEONExT-Hack: apply rounding once only.
554                     if (res.indexOf('toFixed') < 0) {
555                         // output of a value tag
556                         if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) {
557                             // may also be a string
558                             plaintext += '+(' + res + ').toFixed(' + (this.visProp.digits) + ')';
559                         } else {
560                             plaintext += '+(' + res + ')';
561                         }
562                     } else {
563                         plaintext += '+(' + res + ')';
564                     }
565 
566                     contentStr = contentStr.slice(j + 8);
567                     i = contentStr.indexOf('<value>');
568                     j = contentStr.indexOf('</value>');
569                 }
570             }
571 
572             plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"';
573             plaintext = plaintext.replace(/<overline>/g, '<span style=text-decoration:overline>');
574             plaintext = plaintext.replace(/<overline</g, '<span style=text-decoration:overline>');
575             plaintext = plaintext.replace(/<\/overline>/g, '</span>');
576             plaintext = plaintext.replace(/<\/overline>/g, '</span>');
577             plaintext = plaintext.replace(/<arrow>/g, '<span style=text-decoration:overline>');
578             plaintext = plaintext.replace(/<arrow>/g, '<span style=text-decoration:overline>');
579             plaintext = plaintext.replace(/<\/arrow>/g, '</span>');
580             plaintext = plaintext.replace(/<\/arrow>/g, '</span>');
581 
582             // This should replace &pi; by π
583             plaintext = plaintext.replace(/&/g, '&');
584             plaintext = plaintext.replace(/"/g, "'");
585 
586             return plaintext;
587         },
588 
589         /**
590          * Finds dependencies in a given term and notifies the parents by adding the
591          * dependent object to the found objects child elements.
592          * @param {String} content String containing dependencies for the given object.
593          * @private
594          */
595         notifyParents: function (content) {
596             var search,
597                 res = null;
598 
599             // revert possible jc replacement
600             content = content.replace(/<value>/g, '<value>');
601             content = content.replace(/<\/value>/g, '</value>');
602 
603             do {
604                 search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/;
605                 res = search.exec(content);
606 
607                 if (res !== null) {
608                     GeonextParser.findDependencies(this, res[1], this.board);
609                     content = content.substr(res.index);
610                     content = content.replace(search, '');
611                 }
612             } while (res !== null);
613 
614             return this;
615         },
616 
617         bounds: function () {
618             var c = this.coords.usrCoords;
619 
620             return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2] + this.size[1], c[1] + this.size[0], c[2]];
621         },
622 
623         /**
624          * Sets x and y coordinate of the text.
625          * @param {Number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}.
626          * @param {Array} coords coordinates in screen/user units
627          * @param {Array} oldcoords previous coordinates in screen/user units
628          * @returns {JXG.Text} this element
629          */
630         setPositionDirectly: function (method, coords, oldcoords) {
631             var dc, v,
632                 c = new Coords(method, coords, this.board),
633                 oldc = new Coords(method, oldcoords, this.board);
634 
635             if (this.relativeCoords) {
636                 if (this.visProp.islabel) {
637                     dc = Statistics.subtract(c.scrCoords, oldc.scrCoords);
638                     this.relativeCoords.scrCoords[1] += dc[1];
639                     this.relativeCoords.scrCoords[2] += dc[2];
640                 } else {
641                     dc = Statistics.subtract(c.usrCoords, oldc.usrCoords);
642                     this.relativeCoords.usrCoords[1] += dc[1];
643                     this.relativeCoords.usrCoords[2] += dc[2];
644                 }
645             } else {
646                 dc = Statistics.subtract(c.usrCoords, oldc.usrCoords);
647                 v = [this.Z(), this.X(), this.Y()];
648                 this.X = Type.createFunction(v[1] + dc[1], this.board, '');
649                 this.Y = Type.createFunction(v[2] + dc[2], this.board, '');
650             }
651 
652             return this;
653         }
654 
655     });
656 
657     /**
658      * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}.
659      * @pseudo
660      * @description
661      * @name Text
662      * @augments JXG.GeometryElement
663      * @constructor
664      * @type JXG.Text
665      *
666      * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements.
667      *                     <p>
668      *                     x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed,
669      *                     x and y are numbers. The position is variable if x or y are functions.
670      *                     <p>
671      *                     The text to display may be given as string or as function returning a string.
672      *
673      * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
674      * 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.
675      * @see JXG.Text
676      * @example
677      * // Create a fixed text at position [0,1].
678      *   var t1 = board.create('text',[0,1,"Hello World"]);
679      * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
680      * <script type="text/javascript">
681      *   var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
682      *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
683      * </script><pre>
684      * @example
685      * // Create a variable text at a variable position.
686      *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
687      *   var graph = board.create('text',
688      *                        [function(x){ return s.Value();}, 1,
689      *                         function(){return "The value of s is"+s.Value().toFixed(2);}
690      *                        ]
691      *                     );
692      * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
693      * <script type="text/javascript">
694      *   var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
695      *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
696      *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]);
697      * </script><pre>
698      */
699     JXG.createText = function (board, parents, attributes) {
700         var t,
701             attr = Type.copyAttributes(attributes, board.options, 'text');
702 
703         // downwards compatibility
704         attr.anchor = attr.parent || attr.anchor;
705 
706         t = new JXG.Text(board, parents[parents.length - 1], parents, attr);
707 
708         if (typeof parents[parents.length - 1] !== 'function') {
709             t.parents = parents;
710         }
711 
712         if (Type.evaluate(attr.rotate) !== 0 && attr.display === 'internal') {
713             t.addRotation(Type.evaluate(attr.rotate));
714         }
715 
716         return t;
717     };
718 
719     JXG.registerElement('text', JXG.createText);
720 
721     return {
722         Text: JXG.Text,
723         createText: JXG.createText
724     };
725 });
726