Repository

js/Mobilizing/text/TextField.js

import * as _Math from "../core/util/Math";
import Font from "./Font";
import { Raleway } from "../misc/DefaultFonts";
import EventEmitter from "../core/util/EventEmitter";
import StyledLetter from "./StyledLetter";

/**
* Triggered when the canvas has been redrawn, useful to synchronise Texture update and canvas refresh
* @event drawn
*/
const EVT_DRAWN = "drawn";

export default class TextField {
    /**
    * TextField to simulate the input textfield we can find in the DOM.
    * This is implemented in an HTLM Canvas so it can be rendered in a texture.
    *
    * @param {Object} params Parameters object, given by the constructor.
    * @param {Number} [params.maxCharCount = 1000] 
    * @param {Number} [params.size = 20] 
    * @param {CSSColor} [params.color = "gray"] 
    * @param {CSSColor} [params.backgroundColor = "white"] 
    * @param {Number} [params.margins = 10] 
    * @param {Number} [params.width = 300] 
    * @param {Number} [params.height = 100] 
    * @param {Font} [params.font = undefined] 
    * @param {Font} [params.fontItalic = undefined] 
    * @param {Font} [params.fontBold = undefined] 
    * @param {Font} [params.fontBoldItalic = undefined] 
    * @param {CSSColor} [params.cursorColor = "gray"] 
    * @param {Number} [params.cursorWidth = 2] 
    * @param {Number} [params.blinkTime = 300] 
    *
    * @example
    *    //TODO
    */
    constructor({
        maxCharCount = 1000,
        size = 20,
        color = "gray",
        backgroundColor = "white",
        margins = 10,
        width = 300,
        height = 100,
        font = undefined,
        fontItalic = undefined,
        fontBold = undefined,
        fontBoldItalic = undefined,
        cursorColor = "gray",
        cursorWidth = 2,
        blinkTime = 300,
    } = {}) {
        this.maxCharCount = maxCharCount;
        this.size = size;
        this.color = color;
        this.backgroundColor = backgroundColor;
        this.margins = margins;
        this.width = width;
        this.height = height;
        this.font = font;
        this.fontItalic = fontItalic;
        this.fontBold = fontBold;
        this.fontBoldItalic = fontBoldItalic;
        this.cursorColor = cursorColor;
        this.cursorWidth = cursorWidth;
        this.blinkTime = blinkTime;

        this._ready = false;

        this._styledLetters = [];
        this._text = "";

        this._cursorIndex = -1;//to get the position of the cursor from the letters!
        this._cursorX = 0;
        this._cursorY = 0;

        this._canvas = document.createElement("canvas");
        this._canvas.width = this.width;
        this._canvas.height = this.height;
        this._canvasContext = this._canvas.getContext("2d");

        this._canvas.events = new EventEmitter({ scope: this._canvas });

        //document.body.appendChild(this._canvas);
        //this._canvas.style.position = "absolute";

        this.setup();

        this._blinkState = false;
        setInterval(this.onBlink.bind(this), this.blinkTime);
    }

    /**
    * Setup bloc called after default font loading
    * @private
    */
    setup() {
        this._font = (this.font ? this.font : new Font({ base64String: Raleway.regular }));
        this._italicFont = (this.fontItalic ? this.fontItalic : new Font({ base64String: Raleway.italic }));
        this._boldFont = (this.fontBold ? this.fontBold : new Font({ base64String: Raleway.bold }));
        this._boldItalicFont = (this.fontBoldItalic ? this.fontBoldItalic : new Font({ base64String: Raleway.boldItalic }));

        this._currentFont = this._font;

        this.render();
    }

    /**
    * Set the font.
    * @param {Font} font Mobilizing font to use for the next letter
    */
    setFont(font) {
        this._currentFont = font;
    }

    /**
    * Set the size of the next letter
    * @param {Number} size the new size of the font
    */
    setSize(size) {
        this.size = size;
    }

    /**
    * Set the color of the next letter
    * @param {Color} color the new Mobilizing Color
    */
    setColor(color) {
        this.color = color;
    }

    /**
    * add a letter to the field content. Styles (font, color, etc) should be defined before calling this method.
    * @param {String} value the letter to add
    */
    addLetter(value) {

        const char = value.charCodeAt(0);
        let letter = String.fromCharCode(char);

        this._text += letter;

        //manage new line feed
        if (value === "\n") {
            letter = value;
        }

        const styledLetter = new StyledLetter({
            letter,
            "font": this._currentFont,
            "size": this.size,
            "color": this.color
        });

        this._styledLetters.splice(this._cursorIndex + 1, 0, styledLetter);
        this.moveCursorForward();

        this.render();
    }
    
    /**
    * Delete the letter currently before the cursor (or under selection when implemented)
    */
    delete() {
        if (this._cursorIndex >= 0) {
            this._styledLetters.splice(this._cursorIndex, 1);
            //console.log("delete",this._cursorIndex,this._styledLetters);
            this.moveCursorBack();
        }
    }

    /**
     * Clears this textField : erase everything and make the cursor back to the first character place.
     */
    clear(){
        this._cursorIndex = 0;
        this._styledLetters = [];
        this._text = "";
    }

    /**
    * Cursor blink callback
    * @private
    */
    onBlink() {
        this._blinkState = !this._blinkState;
        //console.log("blink",this._blinkState);
        this.render();
    }

    /**
    * Moves the cursor to the next letter
    */
    moveCursorForward() {
        this._cursorIndex++;
        if (this._cursorIndex > this._styledLetters.length - 1) {
            this._cursorIndex = this._styledLetters.length - 1;
        }
        if (this._styledLetters.length === 0) {
            this._cursorIndex = -1;
        }
        this.render();
        //console.log("++this._cursorIndex",this._cursorIndex);
    }

    /**
    * Moves the cursor to the pevious letter
    */
    moveCursorBack() {
        this._cursorIndex--;
        if (this._cursorIndex < -1) {
            this._cursorIndex = -1;
        }
        this.render();
        //console.log("--this._cursorIndex",this._cursorIndex);
    }

    /**
    * Moves the cursor to the given index of the letter
    * @param {Number} index
    */
    moveCursorTo(index) {
        if (index >= -1 && index < this._styledLetters.length) {
            this._cursorIndex = index;
            this.render();
        }
    }

    /**
    * Pick the letter situated under the given x,y coordinates
    * @param {Number} x
    * @param {Number} y
    */
    pickLetter(x, y) {
        for (let i = 0; i < this._styledLetters.length; i++) {
            const el = this._styledLetters[i];
            const bbox = el.path.getBoundingBox();
            const rect = [{ x: bbox.x1, y: bbox.y1 }, { x: bbox.x2, y: bbox.y1 }, { x: bbox.x2, y: bbox.y2 }, { x: bbox.x1, y: bbox.y2 }];

            if (_Math.pointIsInside(x, y, rect)) {
                return { index: i, letter: this._styledLetters[i] };
            }
        }

        return null;
    }

    /**
    * Renders the canvas
    */
    render() {
        //background color
        this._canvasContext.fillStyle = this.backgroundColor;
        this._canvasContext.fillRect(0, 0, this.width, this.height);

        //x position of drawing (letter pos in x)
        let letterXOffset = this.margins;
        //y position of drawing (letter pos in y, or baseline)
        let lineYOffset = this.margins;

        for (let i = 0; i < this._styledLetters.length; i++) {
            if (i < this.maxCharCount) {
                const el = this._styledLetters[i];

                //is it the 1st run ?  place the baseline to the margin + font size
                if (lineYOffset === this.margins) {
                    lineYOffset += el.size;
                }

                //test for new line from canvas width limit and reset offsets and add a new line to lineCount
                const tempWidth = letterXOffset + el.width;

                if (tempWidth > this.width - this.margins) {
                    lineYOffset += el.size;
                    letterXOffset = this.margins;
                }

                //test and manage special case for new line feed
                if (el.letter === "\n") {
                    lineYOffset += el.size;
                    el.height = 0;
                    el.width = 0;
                    letterXOffset = this.margins;

                    el.setX(letterXOffset);
                    el.setY(lineYOffset);
                }
                else {
                    el.setX(letterXOffset);
                    el.setY(lineYOffset);
                    el.update();//refresh
                    el.path.draw(this._canvasContext);
                    //update the x offset for next el
                    letterXOffset += el.width;
                }

            }
        }

        //draw the cursor
        if (this._blinkState) {
            const el = this._styledLetters[this._cursorIndex];

            if (el) {
                const boundingBox = el.path.getBoundingBox();

                this._canvasContext.beginPath();

                //manage blank space
                if (el.height < 1) {
                    this._canvasContext.moveTo(el.x + el.width, el.y);
                    this._canvasContext.lineTo(el.x + el.width, el.y);
                    this._canvasContext.lineTo(el.x + el.width, el.y - this.size);
                }
                //use path boundingBox
                else {
                    this._canvasContext.moveTo(boundingBox.x2, boundingBox.y1);
                    this._canvasContext.lineTo(boundingBox.x2, boundingBox.y1);
                    this._canvasContext.lineTo(boundingBox.x2, boundingBox.y2);
                }
            }
            //we don't have a letter, it's the start of the text
            else {
                this._canvasContext.beginPath();
                this._canvasContext.moveTo(this.margins, this.margins);
                this._canvasContext.lineTo(this.margins, this.margins);
                this._canvasContext.lineTo(this.margins, this.margins + this.size);
            }

            this._canvasContext.lineWidth = this.cursorWidth;
            this._canvasContext.strokeStyle = this.cursorColor;
            this._canvasContext.stroke();
        }

        //emit a custom event on the canvas to refresh texture when used
        this._canvas.events.trigger(EVT_DRAWN);
    }

    /**
    * Get canvas width
    * @return {Number} canvas width
    */
    getWidth() {
        return this.width;
    }
    /**
    * Get canvas height
    * @return {Number} canvas height
    */
    getHeight() {
        return this.height;
    }

    /**
    * Get the canvas
    * @return {Canvas} canvas
    */
    getCanvas() {
        return this._canvas;
    }

    /**
     * Get the text displayed as a string
     * @return {String} the text displayed in this TextField
     */
    getText(){
        return this._text;
    }

}