/**
* File : boxes/TextGrid.js
* Created : 12/06/2015
* By : Francesc Busquets <francesc@gmail.com>
*
* JClic.js
* An HTML5 player of JClic activities
* https://projectestac.github.io/jclic.js
*
* @source https://github.com/projectestac/jclic.js
*
* @license EUPL-1.2
* @licstart
* (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
*
* Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
* the European Commission- subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
*
* You may obtain a copy of the Licence at:
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* Licence for the specific language governing permissions and limitations
* under the Licence.
* @licend
* @module
*/
import { Rectangle, Timer, Point, Dimension, Stroke } from '../AWT.js';
import { roundTo } from '../Utils.js';
import AbstractBox from './AbstractBox.js';
import TextGridContent from './TextGridContent.js';
/**
* Default values
* @type {object}
*/
export const defaults = {
MIN_CELL_SIZE: 12,
DEFAULT_CELL_SIZE: 20,
MIN_INTERNAL_MARGIN: 2
};
/**
* Binary flags used to mark status
* @type {object}
*/
export const flags = {
NORMAL: 0,
INVERTED: 1,
HIDDEN: 2,
LOCKED: 4,
MARKED: 8,
TRANSPARENT: 16
};
/**
* This class is a special type of {@link module:boxes/AbstractBox.AbstractBox AbstractBox} that displays a grid of single
* characters.
*
* It's used {@link module:activities/textGrid/CrossWord.CrossWord CrossWord} and {@link module:activities/textGrid/WordSearch.WordSearch WordSearch} activities.
* @extends module:boxes/AbstractBox.AbstractBox
*/
export class TextGrid extends AbstractBox {
/**
* TextGrid constructor
* @param {module:boxes/AbstractBox.AbstractBox} parent - The AbstractBox to which this text grid belongs
* @param {module:AWT.Container} container - The container where this text grid is placed.
* @param {module:boxes/BoxBase.BoxBase} boxBase - The object where colors, fonts, border and other graphic properties
* @param {number} x - `X` coordinate of the upper left corner of this grid
* @param {number} y - `Y` coordinate of the upper left corner of this grid
* @param {number} ncw - Number of columns of the grid
* @param {number} nch - Nomber of rows of the grid
* @param {number} cellW - Width of the cells
* @param {number} cellH - Height of the cells
* @param {boolean} border - When `true`, a border must be drawn between the cells
*/
constructor(parent, container, boxBase, x, y, ncw, nch, cellW, cellH, border) {
// *TextGrid* extends [AbstractBox](AbstractBox.html)
super(parent, container, boxBase);
this.pos.x = x;
this.pos.y = y;
this.nCols = Math.max(1, ncw);
this.nRows = Math.max(1, nch);
this.cellWidth = Math.max(cellW, defaults.MIN_CELL_SIZE);
this.cellHeight = Math.max(cellH, defaults.MIN_CELL_SIZE);
this.dim.width = cellW * this.nCols;
this.dim.height = cellH * this.nRows;
this.setChars(' ');
this.preferredBounds = new Rectangle(this.pos, this.dim);
this.setBorder(border);
this.cursorTimer = new Timer(() => this.blink(0), 500, false);
this.cursorEnabled = false;
this.useCursor = false;
this.wildTransparent = false;
this.cursor = new Point();
}
/**
* Factory constructor that creates an empty grid based on a {@link module:boxes/TextGridContent.TextGridContent TextGridContent}
* @param {module:boxes/AbstractBox.AbstractBox} parent - The AbstractBox to which the text grid belongs
* @param {module:AWT.Container} container - The container where the text grid will be placed.
* @param {number} x - `X` coordinate of the upper left corner of the grid
* @param {number} y - `Y` coordinate of the upper left corner of the grid
* @param {module:boxes/TextGridContent.TextGridContent} tgc - Object with the content and other settings of the grid
* @param {boolean} wildTransparent - When `true`, the wildcard character will be transparent
* @returns {module:boxes/TextGrid.TextGrid}
*/
static createEmptyGrid(parent, container, x, y, tgc, wildTransparent) {
const result = new TextGrid(parent, container, tgc.style,
x, y, tgc.ncw, tgc.nch, tgc.w, tgc.h, tgc.border);
result.wild = tgc.wild;
result.randomChars = tgc.randomChars;
result.wildTransparent = wildTransparent;
return result;
}
/**
* Sets the characters to be placed in the cells of this TextGrid
* @param {string} text
*/
setChars(text) {
this.chars = [];
this.answers = [];
this.attributes = [];
for (let py = 0; py < this.nRows; py++) {
const line = py < text.length ? text[py] : '';
this.chars[py] = line.split('');
this.answers[py] = [];
this.attributes[py] = [];
for (let px = 0; px < this.nCols; px++) {
if (px >= line.length)
this.chars[py][px] = ' ';
this.answers[py][px] = this.chars[py][px];
this.attributes[py][px] = flags.NORMAL;
}
}
}
/**
* Substitutes the current content of all cells with wildcards with a randomly generated char.
* @see TextGridContent#randomChars
*/
randomize() {
for (let py = 0; py < this.nRows; py++)
for (let px = 0; px < this.nCols; px++)
if (this.chars[py][px] === this.wild)
this.chars[py][px] = this.randomChars.charAt(
Math.floor(Math.random() * this.randomChars.length));
}
/**
* Clears or sets global attributes to all cells
* @param {boolean} lockWild - When `true`, the wildcard cells will be marked with special
* attributes (used in CrossWords to mark black cells)
* @param {boolean} clearChars - When `true`, the current content of cells will be erased.
*/
setCellAttributes(lockWild, clearChars) {
let atr = flags.LOCKED;
if (this.wildTransparent)
atr |= flags.TRANSPARENT;
else
atr |= flags.INVERTED | flags.HIDDEN;
for (let py = 0; py < this.nRows; py++) {
for (let px = 0; px < this.nCols; px++) {
if (lockWild && this.chars[py][px] === this.wild)
this.attributes[py][px] = atr;
else {
this.attributes[py][px] = flags.NORMAL;
if (clearChars)
this.chars[py][px] = ' ';
}
}
}
}
/**
* Sets or unsets the `locked` properties (black cell) to a specific cell.
* @param {number} px - The logical 'X' coordinate of the cell
* @param {number} py - The logical 'Y' coordinate of the cell
* @param {boolean} locked - When true, the `locked` attribute will be on.
*/
setCellLocked(px, py, locked) {
if (px >= 0 && px < this.nCols && py >= 0 && py < this.nRows) {
this.attributes[py][px] = locked ?
flags.LOCKED |
(this.wildTransparent ?
flags.TRANSPARENT :
flags.INVERTED |
flags.HIDDEN) :
flags.NORMAL;
}
}
/**
* For a specific cell located at column `rx` and row `ry`, finds the number of words delimited
* by wildchars located behind its current position and in the same row and column. Used in
* {@link module:activities/textGrid/CrossWord.CrossWord CrossWord} activities to find the definition for a specific cell.
*
* The result is returned as 'x' and 'y' properties of a logical point.
* @param {number} rx - The 'X' position of the cell
* @param {number} ry - The 'Y' position of the cell
* @returns {module:AWT.Point} - The logical positions of the definition for this cell inside the list
* of current definitions of its row and column. '0' means first definition of its row/column,
* '1' the second one, etc.
*/
getItemFor(rx, ry) {
if (!this.isValidCell(rx, ry))
return null;
const point = new Point();
let
inBlack = false,
startCount = false;
for (let px = 0; px < rx; px++) {
if ((this.attributes[ry][px] & flags.LOCKED) !== 0) {
if (!inBlack) {
if (startCount)
point.x++;
inBlack = true;
}
} else {
startCount = true;
inBlack = false;
}
}
inBlack = false;
startCount = false;
for (let py = 0; py < ry; py++) {
if ((this.attributes[py][rx] & flags.LOCKED) !== 0) {
if (!inBlack) {
if (startCount)
point.y++;
inBlack = true;
}
} else {
startCount = true;
inBlack = false;
}
}
return point;
}
/**
* Whether the blinking cursor must be enabled or disabled.
* @param {boolean} status
*/
setCursorEnabled(status) {
this.cursorEnabled = status;
if (status === true)
this.startCursorBlink();
else
this.stopCursorBlink();
}
/**
* Starts the {@link module:AWT.Timer} that makes the cursor blink.
*/
startCursorBlink() {
if (this.useCursor && this.cursorEnabled && this.cursorTimer && !this.cursorTimer.isRunning()) {
this.blink(1);
this.cursorTimer.start();
}
}
/**
* Stops the {@link module:AWT.Timer} that makes the cursor blink.
*/
stopCursorBlink() {
if (this.cursorTimer && this.cursorTimer.isRunning()) {
this.cursorTimer.stop();
this.blink(-1);
}
}
/**
* Moves the cursor in the specified x and y directions.
* @param {number} dx - Amount to move in the 'X' axis
* @param {number} dy - Amount to move in the 'Y' axis
* @param {boolean} skipLocked - Skip locked cells (wildcards in {@link module:activities/textGrid/CrossWord.CrossWord CrossWord})
*/
moveCursor(dx, dy, skipLocked) {
if (this.useCursor) {
const point = this.findNextCellWithAttr(this.cursor.x, this.cursor.y,
skipLocked ? flags.LOCKED : flags.NORMAL,
dx, dy, false);
if (!this.cursor.equals(point))
this.setCursorAt(point.x, point.y, skipLocked);
}
}
/**
* Finds the coordinates of the nearest non-locked cell (non-wildcard) moving on the indicated
* 'X' and 'Y' directions.
* @param {module:AWT.Point} - Logical coordinates of the starting point
* @param {number} dx - 0 means no movement, 1 go right, -1 go left.
* @param {number} dy - 0 means no movement, 1 go down, -1 go up.
* @returns {module:AWT.Point}
*/
findFreeCell(from, dx, dy) {
let result = null;
if (from && (dx !== 0 || dy !== 0)) {
const scan = new Point(from);
while (result === null) {
scan.x += dx;
scan.y += dy;
if (scan.x < 0 || scan.x >= this.nCols || scan.y < 0 || scan.y >= this.nRows)
break;
if (!this.getCellAttribute(scan.x, scan.y, flags.LOCKED))
result = scan;
}
}
return result;
}
/**
* Finds the first cell with the specified attributes at the specified state, starting
* at specified point.
* @param {number} startX - Starting X coordinate
* @param {number} startY - Starting Y coordinate
* @param {number} attr - Attribute to check. See {@link module:boxes/TextGrid.TextGrid.flags}.
* @param {number} dx - 0 means no movement, 1 go right, -1 go left.
* @param {number} dy - 0 means no movement, 1 go down, -1 go up.
* @param {boolean} attrState - Desired state (enabled or disabled) of `attr`
* @returns {module:AWT.Point}
*/
findNextCellWithAttr(startX, startY, attr, dx, dy, attrState) {
const point = new Point(startX + dx, startY + dy);
while (true) {
if (point.x < 0) {
point.x = this.nCols - 1;
if (point.y > 0)
point.y--;
else
point.y = this.nRows - 1;
} else if (point.x >= this.nCols) {
point.x = 0;
if (point.y < this.nRows - 1)
point.y++;
else
point.y = 0;
}
if (point.y < 0) {
point.y = this.nRows - 1;
if (point.x > 0)
point.x--;
else
point.x = this.nCols - 1;
} else if (point.y >= this.nRows) {
point.y = 0;
if (point.x < this.nCols - 1)
point.x++;
else
point.x = 0;
}
if (point.x === startX && point.y === startY ||
this.getCellAttribute(point.x, point.y, attr) === attrState)
break;
point.x += dx;
point.y += dy;
}
return point;
}
/**
* Sets the blinking cursor at a specific point
* @param {number} px - X coordinate
* @param {number} py - Y coordinate
* @param {boolean} skipLocked - Skip locked (wildcard) cells
*/
setCursorAt(px, py, skipLocked) {
this.stopCursorBlink();
if (this.isValidCell(px, py)) {
this.cursor.x = px;
this.cursor.y = py;
this.useCursor = true;
if (skipLocked && this.getCellAttribute(px, py, flags.LOCKED)) {
this.moveCursor(1, 0, skipLocked);
} else {
if (this.cursorEnabled)
this.startCursorBlink();
}
}
}
/**
* Sets the `useCursor` property of this text grid
* @param {boolean} value
*/
setUseCursor(value) {
this.useCursor = value;
}
/**
* Gets the current position of the blinking cursor
* @returns {module:AWT.Point}
*/
getCursor() {
return this.cursor;
}
/**
* Counts the number of cells of this grid with the specified character
* @param {string} ch
* @returns {number}
*/
countCharsLike(ch) {
let result = 0;
for (let py = 0; py < this.nRows; py++)
for (let px = 0; px < this.nCols; px++)
if (this.chars[py][px] === ch)
result++;
return result;
}
/**
* Gets the number of cells of this grid
* @returns {number}
*/
getNumCells() {
return this.nRows * this.nCols;
}
/**
* Counts the number of coincidences between the `answers` array and the current content of this grid
* @param {boolean} checkCase - Make comparisions case-sensitive
* @returns {number}
*/
countCoincidences(checkCase) {
let result = 0;
if (this.answers)
for (let py = 0; py < this.nRows; py++)
for (let px = 0; px < this.nCols; px++)
if (this.isCellOk(px, py, checkCase))
result++;
return result;
}
/**
* Checks if a specific cell is equivalent to the content of `answers` at its position
* @param {number} px - X coordinate
* @param {number} py - Y coordinate
* @param {boolean} checkCase - Make comparisions case-sensitive
* @returns {boolean}
*/
isCellOk(px, py, checkCase) {
let result = false;
if (this.isValidCell(px, py)) {
const ch = this.chars[py][px];
if (ch !== this.wild) {
const ch2 = this.answers[py][px];
if (ch === ch2 ||
!checkCase && ch.toUpperCase() === ch2.toUpperCase())
result = true;
}
}
return result;
}
/**
* Gets the logical coordinates (in 'cell' units) of a device point into the grid
* @param {module:AWT.Point} devicePoint
* @returns {module:AWT.Point}
*/
getLogicalCoords(devicePoint) {
if (!this.contains(devicePoint))
return null;
const
px = Math.floor((devicePoint.x - this.pos.x) / this.cellWidth),
py = Math.floor((devicePoint.y - this.pos.y) / this.cellHeight);
return this.isValidCell(px, py) ? new Point(px, py) : null;
}
/**
* Checks if the specified logical coordinates are inside the valid bounds of the grid.
* @param {number} px - 'X' coordinate
* @param {number} py - 'Y' coordinate
* @returns {boolean}
*/
isValidCell(px, py) {
return px < this.nCols && py < this.nRows && px >= 0 && py >= 0;
}
/**
* Sets the specified character as a content of the cell at specified coordinates
* @param {number} px - 'X' coordinate
* @param {number} py - 'Y' coordinate
* @param {string} ch - The character to set.
*/
setCharAt(px, py, ch) {
if (this.isValidCell(px, py)) {
this.chars[py][px] = ch;
this.repaintCell(px, py);
}
}
/**
* Gets the character of the cell at the specified coordinates
* @param {number} px - 'X' coordinate
* @param {number} py - 'Y' coordinate
* @returns {string}
*/
getCharAt(px, py) {
return this.isValidCell(px, py) ? this.chars[py][px] : ' ';
}
/**
* Gets the text formed by the letters between two cells that share a straight line on the grid.
* The text can be formed horizontally, vertically and diagonal, both in left-to-right or
* right-to-left direction.
* @param {number} x0 - 'X' coordinate of the first cell
* @param {number} y0 - 'Y' coordinate of the first cell
* @param {number} x1 - 'X' coordinate of the second cell
* @param {number} y1 - 'Y' coordinate of the second cell
* @returns {string}
*/
getStringBetween(x0, y0, x1, y1) {
let sb = '';
if (this.isValidCell(x0, y0) && this.isValidCell(x1, y1)) {
let
dx = x1 - x0,
dy = y1 - y0;
if (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)) {
const steps = Math.max(Math.abs(dx), Math.abs(dy));
if (steps > 0) {
dx /= steps;
dy /= steps;
}
for (let i = 0; i <= steps; i++)
sb += this.getCharAt(x0 + dx * i, y0 + dy * i);
}
}
return sb;
}
/**
* Sets a specific attribute to all cells forming a straight line between two cells on the grid.
* @param {number} x0 - 'X' coordinate of the first cell
* @param {number} y0 - 'Y' coordinate of the first cell
* @param {number} x1 - 'X' coordinate of the second cell
* @param {number} y1 - 'Y' coordinate of the second cell
* @param {number} attribute - The binary flag representing this attribute. See {@link module:boxes/TextGrid.TextGrid.flags}.
* @param {boolean} value - Whether to set or unset the attribute.
*/
setAttributeBetween(x0, y0, x1, y1, attribute, value) {
if (this.isValidCell(x0, y0) && this.isValidCell(x1, y1)) {
let
dx = x1 - x0,
dy = y1 - y0;
if (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)) {
const steps = Math.max(Math.abs(dx), Math.abs(dy));
if (steps > 0) {
dx /= steps;
dy /= steps;
}
for (let i = 0; i <= steps; i++)
this.setAttribute(x0 + dx * i, y0 + dy * i, attribute, value);
}
}
}
/**
* Sets or unsets a specifi attrobut to a cell.
* @param {number} px - The 'X' coordinate of the cell
* @param {number} py - The 'Y' coordinate of the cell
* @param {number} attribute - The binary flag representing this attribute. See {@link module:boxes/TextGrid.TextGrid.flags}.
* @param {boolean} state - Whether to set or unset the attribute.
*/
setAttribute(px, py, attribute, state) {
if (this.isValidCell(px, py)) {
if (this.attribute === flags.MARKED && !state)
this.repaintCell(px, py);
this.attributes[py][px] &= ~attribute;
this.attributes[py][px] |= state ? attribute : 0;
if (attribute !== flags.MARKED || state)
this.repaintCell(px, py);
}
}
/**
* Sets the specified attribute to all cells.
* @param {number} attribute - The binary flag representing this attribute. See {@link module:boxes/TextGrid.TextGrid.flags}.
* @param {boolean} state - Whether to set or unset the attribute.
*/
setAllCellsAttribute(attribute, state) {
for (let py = 0; py < this.nRows; py++)
for (let px = 0; px < this.nCols; px++)
this.setAttribute(px, py, attribute, state);
}
/**
* Gets the specified attribute of a cell
* @param {number} px - The 'X' coordinate of the cell
* @param {number} py - The 'Y' coordinate of the cell
* @param {number} attribute - The binary flag representing this attribute. See {@link module:boxes/TextGrid.TextGrid.flags}.
* @returns {boolean} - `true` if the cell has this attribute, `false` otherwise.
*/
getCellAttribute(px, py, attribute) {
return this.isValidCell(px, py) ? (this.attributes[py][px] & attribute) !== 0 : false;
}
/**
* Gets the rectangle enclosing a specific cell
* @param {number} px - The 'X' coordinate of the cell
* @param {number} py - The 'Y' coordinate of the cell
* @returns {module:AWT.Rectangle}
*/
getCellRect(px, py) {
return new Rectangle(this.pos.x + px * this.cellWidth, this.pos.y + py * this.cellHeight, this.cellWidth, this.cellHeight);
}
/**
* Gets the rectangle enclosing a specific cell, including the border thick.
* @param {number} px - The 'X' coordinate of the cell
* @param {number} py - The 'Y' coordinate of the cell
* @returns {module:AWT.Rectangle}
*/
getCellBorderBounds(px, py) {
const isMarked = this.getCellAttribute(px, py, flags.MARKED);
if (!this.border && !isMarked)
return this.getCellRect(px, py);
const
style = this.getBoxBaseResolve(),
strk = isMarked ? style.markerStroke : style.borderStroke;
return this.getCellRect(px, py).grow(strk.lineWidth, strk.lineWidth);
}
/**
* Repaints a cell
* @param {number} px - The 'X' coordinate of the cell
* @param {number} py - The 'Y' coordinate of the cell
*/
repaintCell(px, py) {
if (this.container)
this.container.invalidate(this.getCellBorderBounds(px, py)).update();
}
/**
* Gets the preferred size of this grid
* @returns {module:AWT.Dimension}
*/
getPreferredSize() {
return this.preferredBounds.dim;
}
/**
* Gets the minimum size of this grid
* @returns {module:AWT.Dimension}
*/
getMinimumSize() {
return new Dimension(defaults.MIN_CELL_SIZE * this.nCols, defaults.MIN_CELL_SIZE * this.nRows);
}
/**
* Scales the grid to a new size
* @param {number} scale - The factor used to multiply all coordinates and sizes
* @returns {module:AWT.Dimension}
*/
getScaledSize(scale) {
return new Dimension(
roundTo(scale * this.preferredBounds.dim.width, this.nCols),
roundTo(scale * this.preferredBounds.dim.height, this.nRows));
}
/**
* Overrides {@link module:boxes/AbstractBox.AbstractBox#setBounds}
* @override
* @param {AWT.Rectangle|number} rect - An AWT.Rectangle object, or the `x` coordinate of the
* upper-left corner of a new rectangle.
* @param {number} [y] - `y` coordinate of the upper-left corner of the new rectangle.
* @param {number} [w] - Width of the new rectangle.
* @param {number} [h] - Height of the new rectangle.
*/
setBounds(rect, y, w, h) {
super.setBounds(rect, y, w, h);
this.cellWidth = this.dim.width / this.nCols;
this.cellHeight = this.dim.height / this.nRows;
}
/**
* Overrides {@link module:boxes/AbstractBox.AbstractBox#updateContent}
* @override
* @param {external:CanvasRenderingContext2D} ctx - The canvas rendering context used to draw the
* grid.
* @param {module:AWT.Rectangle} [dirtyRegion] - The area that must be repainted. `null` refers to the whole box.
*/
updateContent(ctx, dirtyRegion) {
const style = this.getBoxBaseResolve();
// test font size
ctx.font = style.font.cssFont();
ctx.textBaseline = 'alphabetic';
style.prepareText(ctx, 'W',
this.cellWidth - 2 * defaults.MIN_INTERNAL_MARGIN,
this.cellHeight - 2 * defaults.MIN_INTERNAL_MARGIN);
const ch = [];
const ry = (this.cellHeight - style.font.getHeight()) / 2 + style.font.getMetrics().ascent;
for (let py = 0; py < this.nRows; py++) {
for (let px = 0; px < this.nCols; px++) {
const bxr = this.getCellBorderBounds(px, py);
if (bxr.intersects(dirtyRegion)) {
const attr = this.attributes[py][px];
if ((attr & flags.TRANSPARENT) === 0) {
const isInverted = (attr & flags.INVERTED) !== 0;
const isMarked = (attr & flags.MARKED) !== 0;
const isCursor = this.useCursor && this.cursor.x === px && this.cursor.y === py;
const boxBounds = this.getCellRect(px, py);
ctx.fillStyle = isCursor && this.cursorBlink ?
style.inactiveColor :
isInverted ? style.textColor : style.backColor;
boxBounds.fill(ctx);
ctx.strokeStyle = 'black';
if ((attr & flags.HIDDEN) === 0) {
ch[0] = this.chars[py][px];
if (ch[0]) {
const dx = boxBounds.pos.x + (this.cellWidth - ctx.measureText(ch[0]).width) / 2;
const dy = boxBounds.pos.y + ry;
if (style.shadow) {
// Render text shadow
const d = Math.max(1, style.font.size / 10);
ctx.fillStyle = style.shadowColor;
ctx.fillText(ch[0], dx + d, dy + d);
}
// Render text
ctx.fillStyle = isInverted ? style.backColor
: this.isAlternative() ? style.alternativeColor : style.textColor;
ctx.fillText(ch[0], dx, dy);
}
}
if (this.border || isMarked) {
ctx.strokeStyle = style.borderColor;
style[isMarked ? 'markerStroke' : 'borderStroke'].setStroke(ctx);
if (isMarked)
ctx.globalCompositeOperation = 'xor';
// Draw border
boxBounds.stroke(ctx);
// Reset ctx default values
if (isMarked)
ctx.globalCompositeOperation = 'source-over';
}
ctx.strokeStyle = 'black';
Stroke.prototype.setStroke(ctx);
}
}
}
}
return true;
}
/**
* Makes the cursor blink, alternating between two states. This method should be called only by
* {@link module:boxes/TextGrid.TextGrid#cursorTimer}
* @param {boolean} status
*/
blink(status) {
// TODO: Move blink and timer to ActivityPanel
if (this.useCursor) {
this.cursorBlink = status === 1 ? true : status === -1 ? false : !this.cursorBlink;
this.repaintCell(this.cursor.x, this.cursor.y);
}
}
/**
* Stops the cursor timer if not `null` and active
*/
end() {
if (this.cursorTimer) {
this.cursorTimer.stop();
this.cursorTimer = null;
}
}
}
Object.assign(TextGrid.prototype, {
/**
* Number of rows
* @name module:boxes/TextGrid.TextGrid#nRows
* @type {number} */
nRows: 1,
/**
* Number of columns
* @name module:boxes/TextGrid.TextGrid#nCols
* @type {number} */
nCols: 1,
/**
* Two-dimension array of characters
* @name module:boxes/TextGrid.TextGrid#chars
* @type {string[][]} */
chars: null,
/**
* Two-dimension array with the expected characters, used to check user's answers.
* @name module:boxes/TextGrid.TextGrid#answers
* @type {string[][]} */
answers: null,
/**
* Two-dimension array of bytes used as containers of boolean attributes
* @name module:boxes/TextGrid.TextGrid#attributes
* @see TextGrid.flags
* @type {number[][]} */
attributes: null,
/**
* The cell width, in pixels
* @name module:boxes/TextGrid.TextGrid#cellWidth
* @type {number} */
cellWidth: 20,
/**
* The cell height, in pixels
* @name module:boxes/TextGrid.TextGrid#cellHeight
* @type {number} */
cellHeight: 20,
/**
* The preferred bounds of this grid
* @name module:boxes/TextGrid.TextGrid#preferredBounds
* @type {module:AWT.Rectangle} */
preferredBounds: null,
/**
* The character to be used as wildcard
* @name module:boxes/TextGrid.TextGrid#wild
* @type {string} */
wild: TextGridContent.prototype.wild,
/**
* Characters that can be used when randomizing the content of this grid
* @name module:boxes/TextGrid.TextGrid#randomChars
* @see TextGridContent#randomChars
* @type {string} */
randomChars: TextGridContent.prototype.randomChars,
/**
* Whether the blinking cursor is enabled or disabled
* @name module:boxes/TextGrid.TextGrid#cursorEnabled
* @type {boolean} */
cursorEnabled: false,
/**
* Whether this grid uses a blinking cursor or not
* @name module:boxes/TextGrid.TextGrid#useCursor
* @type {boolean} */
useCursor: false,
/**
* The current position of the cursor
* @name module:boxes/TextGrid.TextGrid#cursor
* @type {module:AWT.Point} */
cursor: null,
/**
* `true` when the cursor is "blinking" (cell drawn with {@link module:boxes/BoxBase.BoxBase BoxBase} `inverse` attributes)
* @name module:boxes/TextGrid.TextGrid#cursorBlink
* @type {boolean} */
cursorBlink: false,
/**
* Controls the blinking of the cursor
* @name module:boxes/TextGrid.TextGrid#cursorTimer
* @type {module:AWT.Timer} */
cursorTimer: null,
/**
* Whether the wildcard character is transparent or opaque
* @name module:boxes/TextGrid.TextGrid#wildTransparent
* @type {boolean} */
wildTransparent: false,
});
/**
* TextGrid default values
* @name module:boxes/TextGrid.TextGrid.defaults
* @constant
* @type {object} */
TextGrid.defaults = defaults;
/**
* Binary flags used to mark status
* @name module:boxes/TextGrid.TextGrid.flags
* @constant
* @type {object} */
TextGrid.flags = flags;
export default TextGrid;