/**
* File : boxes/BoxBase.js
* Created : 12/04/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 Catalan Educational Telematic Network (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 $ from 'jquery';
import { settings, attrForEach, getBoolean, checkColor, getAttr, setAttr, cloneObject, isSeparator } from '../Utils.js';
import { Stroke, Gradient, Font, Dimension } from '../AWT.js';
const defaultValues = settings.BoxBase;
/**
* This class contains all the main visual attributes needed to draw {@link module:boxes/AbstractBox.AbstractBox AbstractBox} objects:
* background and foreground colors, gradients, colors for special states (inactive, alternative,
* disabled...), margins, fonts, border strokes, etc.
*
* Objects derived from {@link module:boxes/AbstractBox.AbstractBox AbstractBox} can have inheritance: boxes that act as "containers"
* of other boxes (like {@link module:boxes/BoxBag.BoxBag BoxBag}). Most of the attributes of `BoxBase` can be `null`,
* meaning that the value of the ancestor -or the default value if the box has no ancestors- must
* be used.
*/
export class BoxBase {
/**
* BoxBase constructor
* @param {module:boxes/BoxBase.BoxBase} [parent] - Another BoxBase object used to determine the value of properties not
* locally set.
*/
constructor(parent) {
this.parent = parent || null;
}
/**
* Loads the BoxBase settings from a specific JQuery XML element
* @param {external:jQuery} $xml - The XML element to parse
*/
setProperties($xml) {
//
// Read attributes
attrForEach($xml.get(0).attributes, (name, val) => {
switch (name) {
case 'shadow':
case 'transparent':
this[name] = getBoolean(val, false);
break;
case 'margin':
this[name] = Number(val);
break;
case 'borderStroke':
this.borderStroke = new Stroke(Number(val));
break;
case 'markerStroke':
this.markerStroke = new Stroke(Number(val));
break;
}
});
//
// Read inner elements
$xml.children().each((_n, child) => {
const $node = $(child);
switch (child.nodeName) {
case 'font':
this.font = (new Font()).setProperties($node);
break;
case 'gradient':
this.bgGradient = new Gradient().setProperties($node);
break;
case 'color':
this.textColor = checkColor($node.attr('foreground'), this.textColor);
this.backColor = checkColor($node.attr('background'), this.backColor);
this.shadowColor = checkColor($node.attr('shadow'), this.shadowColor);
this.inactiveColor = checkColor($node.attr('inactive'), this.inactiveColor);
this.alternativeColor = checkColor($node.attr('alternative'), this.alternativeColor);
this.borderColor = checkColor($node.attr('border'), this.borderColor);
break;
}
});
return this;
}
/**
* Gets a object with the basic attributes needed to rebuild this instance excluding functions,
* parent references, constants and also attributes retaining the default value.
* The resulting object is commonly usued to serialize elements in JSON format.
* @returns {object} - The resulting object, with minimal attrributes
*/
getAttributes() {
return getAttr(this, [
'shadow', 'transparent', 'margin',
'borderStroke', 'markerStroke', // AWT.Stroke
'font', // AWT.Font
'bgGradient', // AWT.Gradient
`textColor|${BoxBase.prototype.textColor}`,
`backColor|${BoxBase.prototype.backColor}`,
`shadowColor|${BoxBase.prototype.shadowColor}`,
`inactiveColor|${BoxBase.prototype.inactiveColor}`,
`alternativeColor|${BoxBase.prototype.alternativeColor}`,
`borderColor|${BoxBase.prototype.borderColor}`,
]);
}
/**
* Reads the properties of this BoxBase from a data object
* @param {object} data - The data object to be parsed
* @returns {module:boxes/BoxBase.BoxBase}
*/
setAttributes(data) {
return setAttr(this, data, [
'shadow', 'transparent', 'margin',
{ key: 'borderStroke', fn: Stroke },
{ key: 'markerStroke', fn: Stroke },
{ key: 'font', fn: Font },
{ key: 'bgGradient', fn: Gradient },
'textColor',
'backColor',
'shadowColor',
'inactiveColor',
'alternativeColor',
'borderColor',
]);
}
/**
* Gets the value of the specified property, scanning down to parents and prototype if not defined.
* @param {string} property - The property to retrieve
* @returns {any} - The object or value associated to this property
*/
get(property) {
if (this.hasOwnProperty(property) || this.parent === null)
return this[property];
else
return this.parent.get(property);
}
/**
* Sets the value of a specific property.
* @param {string} property - The property name.
* @param {any} value - Depends on the type of property
*/
set(property, value) {
this[property] = value;
return this;
}
/**
* Gets the value of the specified property, scanning down to parents if not defined, and returning
* always an own property (not from prototype)
* @param {string} property - The property to retrieve
* @returns {any} - The object or value associated to this property
*/
getOwn(property) {
if (this.hasOwnProperty(property))
return this[property];
else if (this.parent !== null)
return this.parent.getOwn(property);
else {
if (typeof this[property] === 'object')
this[property] = cloneObject(BoxBase.prototype[property]);
else
this[property] = BoxBase.prototype[property];
}
return this[property];
}
/**
* Gets the properties defined in this BoxBase as a collection of CSS attributes
* @param {object} [css] - An optional set of initial CSS properties
* @param {boolean} [inactive=false] - When `true`, get CSS attributes for an inactive cell
* @param {boolean} [inverse=false] - When `true`, get CSS attributes for an inverse cell
* @param {boolean} [alternative=false] - When `true`, get CSS attributes for an alternative cell
* @returns {object}
*/
getCSS(css, inactive = false, inverse = false, alternative = false) {
// (css will be created by [AWT.Font.toCss](AWT.html) if null or undefined)
const font = this.get('font');
css = font.toCss(css);
css['color'] = inverse ? this.get('backColor')
: alternative ? this.get('alternativeColor')
: this.get('textColor');
const transparent = this.get('transparent');
css['background-color'] = transparent ? 'transparent'
: inactive ? this.get('inactiveColor')
: inverse ? this.get('textColor') : this.get('backColor');
const bgGradient = this.get('bgGradient');
if (bgGradient && !transparent)
css['background-image'] = bgGradient.getCss();
if (this.shadow === 1) {
const delta = Math.max(1, Math.round(font.size / 10));
const color = this.get('shadowColor');
css['text-shadow'] = `${delta}px ${delta}px 3px ${color}`;
}
return css;
}
/**
* This utility method computes the width and height of text lines rendered on an HTML
* __canvas__ element, reducing the font size of the BoxBase as needed when they exceed the maximum
* width and/or height.
* @param {external:CanvasRenderingContext2D} ctx - The canvas rendering context used to draw the text.
* @param {string} text - The text to drawn.
* @param {number} maxWidth - Maximum width
* @param {number} maxHeight - Maximum height
* @returns {object[]} - An array of objects representing lines of text. Each object has a `text`
* member with the text displayed in the line, and a `size` member with the line {@link module:AWT.Dimension}
*/
prepareText(ctx, text, maxWidth, maxHeight) {
const
result = [],
font = this.get('font'),
height = font.getHeight();
let totalHeight = 0;
// divide the text in lines
const lines = text.trim().split('\n');
ctx.font = font.cssFont();
for (let l = 0; l < lines.length; l++) {
let line = lines[l].trim();
let width = ctx.measureText(line).width;
if (width > maxWidth) {
// retain the last string offset that was inside maxWidth
let
lastOK = 0,
lastOKWidth = 0;
for (let p = 0; p < line.length; p++) {
// Find next separator
if (isSeparator(line[p])) {
const w = ctx.measureText(line.substring(0, p).trim()).width;
if (w > maxWidth)
break;
lastOK = p;
lastOKWidth = w;
}
}
if (lastOK > 0) {
// Add a new line with the tail of the line
lines.splice(l + 1, 0, line.substring(lastOK + 1).trim());
// Adjust the current line
line = lines[l] = line.substring(0, lastOK).trim();
width = lastOKWidth;
}
else {
// No solution found. Try resizing down the font.
if (font.size > defaultValues.MIN_FONT_SIZE) {
this.getOwn('font').zoom(-1);
return this.prepareText(ctx, text, maxWidth, maxHeight);
}
}
}
// Add the line and the calculated dimension to `result`
result.push({
text: line,
size: new Dimension(width, height)
});
totalHeight += height;
if (totalHeight > maxHeight && font.size > defaultValues.MIN_FONT_SIZE) {
// Max height exceeded. Try resizing down the font
this.getOwn('font').zoom(-1);
return this.prepareText(ctx, text, maxWidth, maxHeight);
}
}
return result;
}
}
Object.assign(BoxBase.prototype, {
/**
* The parent BoxBase object
* @name module:boxes/BoxBase.BoxBase#parent
* @type {module:boxes/BoxBase.BoxBase} */
parent: null,
/**
* Default values
* @name module:boxes/BoxBase.BoxBase#defaultValues
* @type {object} */
default: defaultValues,
/**
* Font size can be dynamically reduced to fit the available space if any element using this
* `BoxBase` requests it. When this happen, this field contains the real font currently used
* to draw text.
* @name module:boxes/BoxBase.BoxBase#font
* @type {module:AWT.Font} */
font: new Font(),
/**
* The current font size of this BoxBase. Can be dynamically adjusted when drawing.
* @name module:boxes/BoxBase.BoxBase#dynFontSize
* @type {number} */
dynFontSize: 0,
/**
* Counts the number of times the `dynFontSize` has been reset. This is useful to avoid excessive
* recursive loops searching the optimal font size.
* @name module:boxes/BoxBase.BoxBase#resetFontCounter
* @type {number} */
resetFontCounter: 0,
/**
* The background color
* @name module:boxes/BoxBase.BoxBase#backColor
* @type {string} */
backColor: defaultValues.BACK_COLOR,
/**
* The background gradient. Default is `null`.
* @name module:boxes/BoxBase.BoxBase#bgGradient
* @type {module:AWT.Gradient} */
bgGradient: null,
/**
* The color used to write text.
* @name module:boxes/BoxBase.BoxBase#textColor
* @type {string} */
textColor: defaultValues.TEXT_COLOR,
/**
* The color used to draw a shadow below regular text.
* @name module:boxes/BoxBase.BoxBase#shadowColor
* @type {string} */
shadowColor: defaultValues.SHADOW_COLOR,
/**
* The color of the border.
* @name module:boxes/BoxBase.BoxBase#borderColor
* @type {string} */
borderColor: defaultValues.BORDER_COLOR,
/**
* The color used to draw text when a cell is in `inactive` state.
* @name module:boxes/BoxBase.BoxBase#inactiveColor
* @type {string} */
inactiveColor: defaultValues.INACTIVE_COLOR,
/**
* The color used to draw text when a cell is in `alternative` state.
* @name module:boxes/BoxBase.BoxBase#alternativeColor
* @type {string} */
alternativeColor: defaultValues.ALTERNATIVE_COLOR,
/**
* Whether the text should have a shadow or not
* @name module:boxes/BoxBase.BoxBase#shadow
* @type {boolean} */
shadow: false,
/**
* Whether the cell's background (and its hosted component, if any) should be transparent
* @name module:boxes/BoxBase.BoxBase#transparent
* @type {boolean} */
transparent: false,
/**
* Wheter the cell's background should be painted or not. This property has no effect on
* hosted components.
* @name module:boxes/BoxBase.BoxBase#dontFill
* @type {boolean} */
dontFill: false,
/**
* The margin to respect between text elements and the limits of the cell or other elements.
* @name module:boxes/BoxBase.BoxBase#textMargin
* @type {number} */
textMargin: defaultValues.AC_MARGIN,
/**
* The stroke used to draw the border.
* @name module:boxes/BoxBase.BoxBase#borderStroke
* @type {module:AWT.Stroke} */
borderStroke: new Stroke(defaultValues.BORDER_STROKE_WIDTH),
/**
* The stroke used to draw a border around marked cells.
* @name module:boxes/BoxBase.BoxBase#markerStroke
* @type {module:AWT.Stroke} */
markerStroke: new Stroke(defaultValues.MARKER_STROKE_WIDTH),
});
BoxBase.DEFAULT_BOX_BASE = new BoxBase();
export default BoxBase;