/**
* File : boxes/AbstractBox.js
* Created : 18/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 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, Point, Dimension, Stroke } from '../AWT.js';
import BoxBase from './BoxBase.js';
/**
* This abstract class is the base for most graphic components of JClic. It describes an area
* (by default an {@link module:AWT.Rectangle}) with some special properties that determine how it must
* be drawn on screen.
*
* Some types of boxes can act as containers for other boxes, establishing a hierarchy of dependences.
* @abstract
* @extends module:AWT.Rectangle
*/
export class AbstractBox extends Rectangle {
/**
* AbstractBox constructor
* @param {module:AbstractBox} parent - The AbstractBox to which this one belongs
* @param {module:AWT.Container} container - The container where this box is placed.
* @param {module:BoxBase} boxBase - The object where colors, fonts, border and other graphic properties
* of this box are defined.
*/
constructor(parent, container, boxBase) {
// AbstractBox extends AWT.Rectangle
super();
this.container = container;
this.parent = parent;
this.boxBase = boxBase;
this.shape = this;
this.specialShape = false;
this.visible = true;
}
/**
* Setter method for `parent`
* @param {module:boxes/AbstractBox.AbstractBox} parent - The new parent of this box
*/
setParent(parent) {
this.parent = parent;
}
/**
* Gets the current parent of this box
* @returns {module:boxes/AbstractBox.AbstractBox}
*/
getParent() {
return this.parent;
}
/**
* Finisher method
*/
end() {
}
/**
* Setter method for `container`
* @param {module:AWT.Container} newContainer - The new Container assigned to this box
*/
setContainer(newContainer) {
this.container = newContainer;
if (this.$hostedComponent && this.container && this.container.$div) {
this.$hostedComponent.detach();
this.container.$div.append(this.$hostedComponent);
}
}
/**
* Gets the `container` attribute of this box, without checking its parent
* @returns {module:AWT.Container}
*/
getContainerX() {
return this.container;
}
/**
* Gets the container associated to this box, asking its parents when `null`.
* @returns {module:AWT.Container}
*/
getContainerResolve() {
let ab = this;
while (ab.container === null && ab.parent !== null)
ab = ab.parent;
return ab.container;
}
/**
* Invalidates the zone corresponding to this box in the associated {@link module:AWT.Container}, if any.
* @param {module:AWT.Rectangle} rect - The rectangle to be invalidated. When `null`, it's the full
* container area.
*/
invalidate(rect) {
const cnt = this.getContainerResolve();
if (cnt)
cnt.invalidate(rect);
}
/**
* Sets the {@link module:boxes/BoxBase.BoxBase BoxBase} of this box
* @param {module:boxes/BoxBase.BoxBase} boxBase - The new BoxBase
*/
setBoxBase(boxBase) {
this.boxBase = boxBase;
this.invalidate();
}
/**
* Gets the real {@link module:boxes/BoxBase.BoxBase BoxBase} associated to this box, scanning down parent relationships.
* @returns {module:boxes/BoxBase.BoxBase}
*/
getBoxBaseResolve() {
let ab = this;
while (!ab.boxBase && ab.parent)
ab = ab.parent;
return ab.boxBase || BoxBase.DEFAULT_BOX_BASE;
}
/**
* Sets the shape used to draw the content of this box
* @param {module:AWT.Shape} sh - The shape to be set
*/
setShape(sh) {
this.shape = sh;
this.specialShape = true;
this.invalidate();
super.setBounds(sh.getBounds());
this.invalidate();
}
/**
* Gets the current shape used in this box
* @returns {module:AWT.Shape}
*/
getShape() {
return this.shape;
}
/**
* Check if this box contains the specified point
* @override
* @param {module:AWT.Point} p - The point to be checked
* @returns {boolean}
*/
contains(p) {
return this.shape === this ? super.contains(p) : this.shape.contains(p);
}
/**
* Sets a new size and/or dimension to this box
* @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) {
if (typeof rect === 'number')
// arguments are co-ordinates and size
rect = new Rectangle(rect, y, w, h);
// Rectangle comparision
if (this.equals(rect))
return;
const sizeChanged = !this.dim.equals(rect.dim);
if (this.specialShape) {
if (sizeChanged) {
this.shape.scaleBy(new Dimension(rect.dim.width / this.dim.width, rect.dim.height / this.dim.height));
this.setShape(this.shape);
}
if (!this.pos.equals(rect.pos)) {
this.shape.moveTo(rect.pos);
}
this.setShape(this.shape);
} else
super.setBounds(rect);
if (this.$hostedComponent)
this.setHostedComponentBounds(sizeChanged);
return this;
}
/**
* Sets a new location for this box. In JClic this method was named `setLocation`
* @param {AWT.Point|number} newPos - A point or the `x` coordinate of a new point.
* @param {number} [y] - The `y` coordinate of a new point.
*/
moveTo(newPos, y) {
if (typeof newPos === 'number')
newPos = new Point(newPos, y);
this.setBounds((new Rectangle(this)).moveTo(newPos));
}
/**
* Sets a new location to this box. In JClic this method was named `translate`.
* @param {number} dx - The displacement on the X axis
* @param {number} dy - The displacement on the Y axis
*/
moveBy(dx, dy) {
this.setBounds((new Rectangle(this)).moveBy(dx, dy));
}
/**
* Changes the size of this box
* @param {number} width
* @param {number} height
*/
setSize(width, height) {
this.setBounds(new Rectangle(this.pos, new Dimension(width, height)));
}
/**
* Checks if this box has border
* @returns {boolean}
*/
hasBorder() {
return this.border;
}
/**
* Sets/unsets a border to this box
* @param {boolean} newVal - `true` to set a border.
*/
setBorder(newVal) {
if (!newVal)
this.invalidate();
this.border = newVal;
if (newVal)
this.invalidate();
}
/**
* Checks if this box is fully visible
* @returns {boolean}
*/
isVisible() {
return this.visible;
}
/**
* Sets this box visible or invisible
* @param {boolean} newVal - `true` for visible
*/
setVisible(newVal) {
this.visible = newVal;
this.setHostedComponentVisible();
this.invalidate();
}
/**
* Makes {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent} visible or invisible, based on the value of
* the AbstractBox `visible` flag.
*/
setHostedComponentVisible() {
if (this.$hostedComponent)
this.$hostedComponent.css('visibility', this.visible ? 'visible' : 'hidden');
}
/**
* Checks if this box is temporary hidden
* @returns {boolean}
*/
isTemporaryHidden() {
return this.temporaryHidden;
}
/**
* Makes this box temporary hidden (newVal `true`) or resets its original state (newVal `false`)
* @param {boolean} newVal
*/
setTemporaryHidden(newVal) {
this.temporaryHidden = newVal;
}
/**
* Checks if this box is currently inactive.
* @returns {boolean}
*/
isInactive() {
return this.inactive;
}
/**
* Makes this box active (`false`) or inactive (`true`)
* @param {boolean} newVal
*/
setInactive(newVal) {
this.inactive = newVal;
if (this.$hostedComponent) {
this.setHostedComponentColors();
this.setHostedComponentVisible();
} else {
if (this.$accessibleElement) {
const disabled = this.isInactive() && !this.accessibleAlwaysActive;
this.$accessibleElement.prop({
disabled: disabled,
tabindex: disabled ? -1 : 0
});
}
this.invalidate();
}
}
/**
* Checks if this box is in `inverted` state.
* @returns {boolean}
*/
isInverted() {
return this.inverted;
}
/**
* Puts this box in `inverted` mode or restores its original state.
* @param {boolean} newVal
*/
setInverted(newVal) {
this.inverted = newVal;
if (this.$hostedComponent)
this.setHostedComponentColors();
else
this.invalidate();
}
/**
* Checks if this box is `marked`
* @returns {boolean}
*/
isMarked() {
return this.marked;
}
/**
* Sets this box in `marked` mode, or restores its original state.
* @param {boolean} newVal
*/
setMarked(newVal) {
if (!newVal)
this.invalidate();
this.marked = newVal;
if (this.$hostedComponent) {
this.setHostedComponentColors();
this.setHostedComponentBorder();
} else if (newVal)
this.invalidate();
}
/**
* Checks if this box has the input focus
* @returns {boolean}
*/
isFocused() {
return this.focused;
}
/**
*
* Sets or unsets the input focus to this box.
* @param {boolean} newVal
*/
setFocused(newVal) {
if (!newVal)
this.invalidate();
this.focused = newVal;
if (newVal)
this.invalidate();
// Put hosted component on top
if (this.$hostedComponent)
this.$hostedComponent.css('z-index', this.focused ? 20 : 2);
}
/**
* Checks if this box is in `alternative` state.
* @returns {boolean}
*/
isAlternative() {
return this.alternative;
}
/**
* Sets this box in `alternative` mode, or restores its original state.
* @param {boolean} newVal
*/
setAlternative(newVal) {
this.alternative = newVal;
this.invalidate();
}
/**
* Draws the content of this box on an HTML `canvas` element. At this level, only background
* and border are painted/stroked. Derived classes should implement specific drawing tasks in
* {@link module:boxes/AbstractBox.AbstractBox#updateContent}.
* @param {external:CanvasRenderingContext2D} ctx - The canvas rendering context used to draw the
* box content.
* @param {module:AWT.Rectangle} [dirtyRegion=null] - The area that must be repainted. `null` refers to the whole box.
*/
update(ctx, dirtyRegion = null) {
if (this.isEmpty() || !this.isVisible() || this.isTemporaryHidden())
return false;
if (dirtyRegion && !this.shape.intersects(dirtyRegion))
return false;
/**
* TODO: Implement clipping
Shape saveClip=new Area(g2.getClip())
Area clip=new Area(saveClip)
clip.intersect(new Area(shape))
g2.setClip(clip)
*/
const style = this.getBoxBaseResolve();
if (!style.transparent && !style.dontFill && !this.tmpTrans) {
if (!style.bgGradient || style.bgGradient.hasTransparency()) {
// Prepare the rendering context
ctx.fillStyle = this.inactive ?
style.inactiveColor :
this.inverted ? style.textColor : style.backColor;
// Fill the shape
this.shape.fill(ctx, dirtyRegion);
}
if (style.bgGradient) {
ctx.fillStyle = style.bgGradient.getGradient(ctx, this.shape.getBounds());
this.shape.fill(ctx, dirtyRegion);
}
// Reset the canvas context
ctx.fillStyle = 'black';
}
if (!this.$hostedComponent)
this.updateContent(ctx, dirtyRegion);
this.drawBorder(ctx);
return true;
}
/**
* Here is where classes derived from {@link module:boxes/AbstractBox.AbstractBox AbstractBox} should implement the drawing of its
* content. Background and border are already painted in {@link module:boxes/AbstractBox.AbstractBox#update}.
* @param {external:CanvasRenderingContext2D} _ctx - The canvas rendering context used to draw the
* box content.
* @param {module:AWT.Rectangle} [_dirtyRegion] - The area that must be repainted. `null` refers to the whole box.
*/
//
// Abstract method, to be implemented in subclasses
updateContent(_ctx, _dirtyRegion) {
}
/**
* Draws the box border
* @param {external:CanvasRenderingContext2D} ctx - The canvas rendering context where the border
* will be drawn.
*/
drawBorder(ctx) {
if (this.border || this.marked) {
const style = this.getBoxBaseResolve();
// Prepare stroke settings
ctx.strokeStyle = style.borderColor;
style[this.marked ? 'markerStroke' : 'borderStroke'].setStroke(ctx);
if (this.marked)
ctx.globalCompositeOperation = 'xor';
// Draw border
this.shape.stroke(ctx);
// Reset ctx default values
if (this.marked)
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'black';
Stroke.prototype.setStroke(ctx);
}
}
/**
* Returns the enclosing Rectangle of this box including its border (if any)
* @returns {module:AWT.Rectangle}
*/
getBorderBounds() {
const result = new Rectangle(this.getBounds());
if (this.border || this.marked) {
const style = this.getBoxBaseResolve();
const w = style[this.marked ? 'markerStroke' : 'borderStroke'].lineWidth;
result.moveBy(-w / 2, -w / 2);
result.dim.width += w;
result.dim.height += w;
}
return result;
}
/**
* Sets the {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent $hostedComponent} member.
* @param {external:jQuery} $hc - The jQuery DOM component hosted by this box.
*/
setHostedComponent($hc) {
if (this.$hostedComponent)
this.$hostedComponent.detach();
this.$hostedComponent = $hc;
if (this.$hostedComponent) {
this.setContainer(this.container);
this.setHostedComponentColors();
this.setHostedComponentBorder();
this.setHostedComponentBounds(true);
this.setHostedComponentVisible();
this.setFocused(this.focused);
}
}
/**
* Gets the current {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent|$hostedComponent} member
* @returns {external:jQuery}
*/
getHostedComponent() {
return this.$hostedComponent;
}
/**
* Sets {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent|$hostedComponent} colors and other css properties
* based on the current {@link module:boxes/BoxBase.BoxBase BoxBase} of this box.
*/
setHostedComponentColors() {
if (this.$hostedComponent) {
const style = this.getBoxBaseResolve();
const css = style.getCSS(null, this.inactive, this.inverted, this.alternative);
// Check if cell has background gradient and animated gif
if (this.$hostedComponent.data('background-image') && css['background-image'])
css['background-image'] = `${this.$hostedComponent.data('background-image')},${css['background-image']}`;
this.$hostedComponent.css(css);
}
}
/**
* Sets the {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent|$hostedComponent} border, based on the current
* {@link module:boxes/BoxBase.BoxBase BoxBase} of this box.
*/
setHostedComponentBorder() {
if (this.$hostedComponent && (this.border || this.marked)) {
const style = this.getBoxBaseResolve();
this.$hostedComponent.css({
'border-width': `${style.get(this.marked ? 'markerStroke' : 'borderStroke').lineWidth}px`,
'border-style': 'solid',
'border-color': style.get('borderColor')
});
}
}
/**
* Places and resizes {@link module:boxes/AbstractBox.AbstractBox#$hostedComponent|$hostedComponent}, based on the size
* and position of this box.
* @param {boolean} _sizeChanged - `true` when this {@link module:boxes/ActiveBox.ActiveBox ActiveBox} has changed its size
*/
setHostedComponentBounds(_sizeChanged) {
if (this.$hostedComponent) {
const
r = this.getBounds(),
b = this.border || this.marked ? this.getBoxBaseResolve().get(this.marked ? 'markerStroke' : 'borderStroke').lineWidth : 0;
this.$hostedComponent.css({
position: 'absolute',
width: r.dim.width - 2 * b + 'px',
height: r.dim.height - 2 * b + 'px',
top: r.pos.y + 'px',
left: r.pos.x + 'px'
});
}
}
}
Object.assign(AbstractBox.prototype, {
/**
* The parent AbstractBox (can be `null`)
* @name module:boxes/AbstractBox.AbstractBox#parent
* @type {module:boxes/AbstractBox.AbstractBox} */
parent: null,
/**
* The Container to which this AbstractBox belongs
* @name module:boxes/AbstractBox.AbstractBox#container
* @type {module:AWT.Container} */
container: null,
/**
* The {@link module:boxes/BoxBase.BoxBase BoxBase} related to this AbstractBox. When `null`, the parent can provide an
* alternative one.
* @name module:boxes/AbstractBox.AbstractBox#boxBase
* @type {module:boxes/BoxBase.BoxBase} */
boxBase: null,
/**
* Whether this box has a border or not
* @name module:boxes/AbstractBox.AbstractBox#border
* @type {boolean} */
border: false,
/**
* The shape of this box (the box Rectangle or a special Shape, if set)
* @name module:boxes/AbstractBox.AbstractBox#shape
* @type {module:AWT.Shape} */
shape: null,
/**
* Whether this box has a shape that is not a rectangle
* @name module:boxes/AbstractBox.AbstractBox#specialShape
* @type {boolean} */
specialShape: false,
/**
* Whether this box is visible or not
* @name module:boxes/AbstractBox.AbstractBox#visible
* @type {boolean} */
visible: true,
/**
* Used to temporary hide a box while other drawing operations are done
* @name module:boxes/AbstractBox.AbstractBox#temporaryHidden
* @type {boolean} */
temporaryHidden: false,
/**
* Cells with this attribute will be transparent but with painted border
* @name module:boxes/AbstractBox.AbstractBox#tmpTrans
* @type {boolean}*/
tmpTrans: false,
/**
* Whether this box is active or inactive
* @name module:boxes/AbstractBox.AbstractBox#inactive
* @type {boolean} */
inactive: false,
/**
* Whether this box must be displayed with inverted or regular colors
* @name module:boxes/AbstractBox.AbstractBox#inverted
* @type {boolean} */
inverted: false,
/**
* Whether this box must be displayed with alternative or regular color and font settings
* @name module:boxes/AbstractBox.AbstractBox#alternative
* @type {boolean} */
alternative: false,
/**
* Whether this box is marked (selected) or not
* @name module:boxes/AbstractBox.AbstractBox#marked
* @type {boolean} */
marked: false,
/**
* Whether this box holds the input focus
* @name module:boxes/AbstractBox.AbstractBox#focused
* @type {boolean} */
focused: false,
/**
* Text to be used in accessible contexts
* @name module:boxes/AbstractBox.AbstractBox#accessibleText
* @type {string} */
accessibleText: '',
/**
* Describes the main role of this box on the activity. Useful in wai-aria descriptions.
* @name module:boxes/AbstractBox.AbstractBox#role
* @type {string} */
role: 'cell',
/**
* DOM element used to display this cell content in wai-aria contexts
* @name module:boxes/AbstractBox.AbstractBox#$accessibleElement
* @type {external:jQuery} */
$accessibleElement: null,
/**
* Flag indicating that $accessibleElement should be always active
* @name module:boxes/AbstractBox.AbstractBox#accessibleAlwaysActive
* @type {boolean} */
accessibleAlwaysActive: false,
/**
* An external JQuery DOM element hosted by this box
* @name module:boxes/AbstractBox.AbstractBox#$hostedComponent
* @type {external:jQuery} */
$hostedComponent: null,
});
export default AbstractBox;