konva/src/Container.ts

639 lines
16 KiB
TypeScript
Raw Normal View History

2019-01-02 04:59:27 +08:00
import { Util, Collection } from './Util';
2019-02-25 01:06:04 +08:00
import { Factory } from './Factory';
2019-03-19 03:18:03 +08:00
import { Node, NodeConfig } from './Node';
2019-02-24 10:36:05 +08:00
import { DD } from './DragAndDrop';
2019-02-25 01:06:04 +08:00
import { getNumberValidator } from './Validators';
2019-08-04 10:41:57 +08:00
import { Konva } from './Global';
2019-01-02 04:59:27 +08:00
2019-01-25 11:52:16 +08:00
import { GetSet, IRect } from './types';
2019-04-09 01:17:26 +08:00
import { Shape } from './Shape';
import { HitCanvas, SceneCanvas } from './Canvas';
2019-01-02 04:59:27 +08:00
export interface ContainerConfig extends NodeConfig {
clearBeforeDraw?: boolean;
clipFunc?: (ctx: CanvasRenderingContext2D) => void;
clipX?: number;
clipY?: number;
clipWidth?: number;
clipHeight?: number;
}
2019-01-02 04:59:27 +08:00
/**
* Container constructor.  Containers are used to contain nodes or other containers
* @constructor
* @memberof Konva
* @augments Konva.Node
* @abstract
* @param {Object} config
* @@nodeParams
* @@containerParams
*/
2019-04-17 23:45:47 +08:00
export abstract class Container<ChildType extends Node> extends Node<
ContainerConfig
> {
children = new Collection<ChildType>();
2019-01-02 04:59:27 +08:00
/**
* returns a {@link Konva.Collection} of direct descendant nodes
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#getChildren
2019-01-02 04:59:27 +08:00
* @param {Function} [filterFunc] filter function
* @returns {Konva.Collection}
* @example
* // get all children
* var children = layer.getChildren();
*
* // get only circles
* var circles = layer.getChildren(function(node){
* return node.getClassName() === 'Circle';
* });
*/
getChildren(filterFunc?: (item: Node) => boolean) {
if (!filterFunc) {
return this.children;
}
var results = new Collection();
this.children.each(function (child) {
2019-01-02 04:59:27 +08:00
if (filterFunc(child)) {
results.push(child);
}
});
return results;
}
/**
* determine if node has children
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#hasChildren
2019-01-02 04:59:27 +08:00
* @returns {Boolean}
*/
hasChildren() {
return this.getChildren().length > 0;
}
/**
* remove all children. Children will be still in memory.
* If you want to completely destroy all children please use "destroyChildren" method instead
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#removeChildren
2019-01-02 04:59:27 +08:00
*/
removeChildren() {
var child;
2019-02-23 01:46:46 +08:00
for (var i = 0; i < this.children.length; i++) {
child = this.children[i];
2019-01-02 04:59:27 +08:00
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
2019-01-02 04:59:27 +08:00
child.index = 0;
child.remove();
}
this.children = new Collection();
return this;
}
/**
* destroy all children nodes.
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#destroyChildren
2019-01-02 04:59:27 +08:00
*/
destroyChildren() {
var child;
2019-02-23 01:46:46 +08:00
for (var i = 0; i < this.children.length; i++) {
child = this.children[i];
2019-01-02 04:59:27 +08:00
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
2019-01-02 04:59:27 +08:00
child.index = 0;
child.destroy();
}
this.children = new Collection();
return this;
}
abstract _validateAdd(node: Node): void;
/**
2019-01-06 16:01:20 +08:00
* add a child and children into container
* @name Konva.Container#add
2019-01-02 04:59:27 +08:00
* @method
* @param {...Konva.Node} child
* @returns {Container}
* @example
2019-01-06 16:01:20 +08:00
* layer.add(rect);
2019-01-02 04:59:27 +08:00
* layer.add(shape1, shape2, shape3);
2019-01-06 16:01:20 +08:00
* // remember to redraw layer if you changed something
* layer.draw();
2019-01-02 04:59:27 +08:00
*/
add(...children: ChildType[]) {
2019-01-02 04:59:27 +08:00
if (arguments.length > 1) {
for (var i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
return this;
}
var child = children[0];
2019-01-02 04:59:27 +08:00
if (child.getParent()) {
child.moveTo(this);
return this;
}
var _children = this.children;
2019-01-02 04:59:27 +08:00
this._validateAdd(child);
child._clearCaches();
child.index = _children.length;
2019-01-02 04:59:27 +08:00
child.parent = this;
_children.push(child);
2019-01-02 04:59:27 +08:00
this._fire('add', {
child: child,
2019-01-02 04:59:27 +08:00
});
// chainable
return this;
}
destroy() {
if (this.hasChildren()) {
this.destroyChildren();
}
2019-01-06 16:01:20 +08:00
super.destroy();
2019-01-02 04:59:27 +08:00
return this;
}
/**
* return a {@link Konva.Collection} of nodes that match the selector.
* You can provide a string with '#' for id selections and '.' for name selections.
* Or a function that will return true/false when a node is passed through. See example below.
* With strings you can also select by type or class name. Pass multiple selectors
* separated by a space.
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#find
2019-01-02 04:59:27 +08:00
* @param {String | Function} selector
* @returns {Collection}
* @example
*
* Passing a string as a selector
* // select node with id foo
* var node = stage.find('#foo');
*
* // select nodes with name bar inside layer
* var nodes = layer.find('.bar');
*
* // select all groups inside layer
* var nodes = layer.find('Group');
*
* // select all rectangles inside layer
* var nodes = layer.find('Rect');
*
* // select node with an id of foo or a name of bar inside layer
* var nodes = layer.find('#foo, .bar');
*
* Passing a function as a selector
*
2019-01-06 16:01:20 +08:00
* // get all groups with a function
2019-01-02 04:59:27 +08:00
* var groups = stage.find(node => {
* return node.getType() === 'Group';
* });
*
* // get only Nodes with partial opacity
* var alphaNodes = layer.find(node => {
* return node.getType() === 'Node' && node.getAbsoluteOpacity() < 1;
* });
*/
2020-03-29 20:45:24 +08:00
find<ChildNode extends Node = Node>(selector): Collection<Node> {
2019-01-02 04:59:27 +08:00
// protecting _generalFind to prevent user from accidentally adding
// second argument and getting unexpected `findOne` result
2019-04-22 21:17:25 +08:00
return this._generalFind<ChildNode>(selector, false);
2019-01-02 04:59:27 +08:00
}
2019-01-06 16:01:20 +08:00
get(selector) {
Util.warn(
'collection.get() method is deprecated. Please use collection.find() instead.'
);
return this.find(selector);
}
2019-01-02 04:59:27 +08:00
/**
* return a first node from `find` method
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#findOne
2019-01-02 04:59:27 +08:00
* @param {String | Function} selector
* @returns {Konva.Node | Undefined}
* @example
* // select node with id foo
* var node = stage.findOne('#foo');
*
* // select node with name bar inside layer
* var nodes = layer.findOne('.bar');
*
* // select the first node to return true in a function
* var node = stage.findOne(node => {
* return node.getType() === 'Shape'
* })
*/
2020-07-06 23:20:47 +08:00
findOne<ChildNode extends Node = Node>(selector: string | Function) {
2019-04-22 21:17:25 +08:00
var result = this._generalFind<ChildNode>(selector, true);
2019-01-02 04:59:27 +08:00
return result.length > 0 ? result[0] : undefined;
}
2020-03-29 20:45:24 +08:00
_generalFind<ChildNode extends Node = Node>(
selector: string | Function,
findOne: boolean
) {
2020-07-06 23:20:47 +08:00
var retArr: Array<ChildNode> = [];
2019-01-02 04:59:27 +08:00
2020-07-06 23:20:47 +08:00
this._descendants((node: ChildNode) => {
2019-03-19 03:18:03 +08:00
const valid = node._isMatch(selector);
if (valid) {
retArr.push(node);
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
if (valid && findOne) {
return true;
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
return false;
});
2019-01-02 04:59:27 +08:00
2020-07-06 23:20:47 +08:00
return Collection.toCollection<ChildNode>(retArr);
2019-01-02 04:59:27 +08:00
}
2020-03-29 20:45:24 +08:00
private _descendants(fn: (n: Node) => boolean) {
2019-03-19 03:18:03 +08:00
let shouldStop = false;
for (var i = 0; i < this.children.length; i++) {
const child = this.children[i];
shouldStop = fn(child);
if (shouldStop) {
return true;
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
if (!child.hasChildren()) {
continue;
2019-01-02 04:59:27 +08:00
}
2019-04-17 23:45:47 +08:00
shouldStop = (child as any)._descendants(fn);
2019-03-19 03:18:03 +08:00
if (shouldStop) {
return true;
2019-01-02 04:59:27 +08:00
}
}
2019-03-19 03:18:03 +08:00
return false;
2019-01-02 04:59:27 +08:00
}
// extenders
toObject() {
var obj = Node.prototype.toObject.call(this);
obj.children = [];
var children = this.getChildren();
var len = children.length;
for (var n = 0; n < len; n++) {
var child = children[n];
obj.children.push(child.toObject());
}
return obj;
}
/**
* determine if node is an ancestor
* of descendant
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#isAncestorOf
2019-01-02 04:59:27 +08:00
* @param {Konva.Node} node
*/
2020-03-29 20:45:24 +08:00
isAncestorOf(node: Node) {
2019-01-02 04:59:27 +08:00
var parent = node.getParent();
while (parent) {
if (parent._id === this._id) {
return true;
}
parent = parent.getParent();
}
return false;
}
2020-03-29 20:45:24 +08:00
clone(obj?: any) {
2019-01-02 04:59:27 +08:00
// call super method
var node = Node.prototype.clone.call(this, obj);
this.getChildren().each(function (no) {
2019-01-02 04:59:27 +08:00
node.add(no.clone());
});
return node;
}
/**
* get all shapes that intersect a point. Note: because this method must clear a temporary
2019-01-06 16:01:20 +08:00
* canvas and redraw every shape inside the container, it should only be used for special situations
2019-01-02 04:59:27 +08:00
* because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible
* because it performs much better
* @method
* @name Konva.Container#getAllIntersections
2019-01-02 04:59:27 +08:00
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Array} array of shapes
*/
getAllIntersections(pos) {
var arr = [];
this.find('Shape').each(function (shape: Shape) {
2019-01-02 04:59:27 +08:00
if (shape.isVisible() && shape.intersects(pos)) {
arr.push(shape);
}
});
return arr;
}
_setChildrenIndices() {
this.children.each(function (child, n) {
2019-01-02 04:59:27 +08:00
child.index = n;
});
}
drawScene(can?: SceneCanvas, top?: Node) {
2019-01-02 04:59:27 +08:00
var layer = this.getLayer(),
canvas = can || (layer && layer.getCanvas()),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
2019-01-02 04:59:27 +08:00
cachedSceneCanvas = cachedCanvas && cachedCanvas.scene;
var caching = canvas && canvas.isCache;
if (!this.isVisible() && !caching) {
return this;
}
if (cachedSceneCanvas) {
context.save();
2020-06-11 00:57:48 +08:00
var m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedSceneCanvas(context);
context.restore();
} else {
this._drawChildren('drawScene', canvas, top);
2019-01-02 04:59:27 +08:00
}
return this;
}
drawHit(can?: HitCanvas, top?: Node) {
if (!this.shouldDrawHit(top)) {
return this;
}
2019-01-02 04:59:27 +08:00
var layer = this.getLayer(),
canvas = can || (layer && layer.hitCanvas),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
2019-01-02 04:59:27 +08:00
cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (cachedHitCanvas) {
context.save();
2020-06-11 00:57:48 +08:00
var m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedHitCanvas(context);
context.restore();
} else {
this._drawChildren('drawHit', canvas, top);
2019-01-02 04:59:27 +08:00
}
return this;
}
_drawChildren(drawMethod, canvas, top) {
2020-06-11 00:57:48 +08:00
var context = canvas && canvas.getContext(),
2019-01-02 04:59:27 +08:00
clipWidth = this.clipWidth(),
clipHeight = this.clipHeight(),
clipFunc = this.clipFunc(),
hasClip = (clipWidth && clipHeight) || clipFunc;
const selfCache = top === this;
2019-01-02 04:59:27 +08:00
if (hasClip) {
2019-01-02 04:59:27 +08:00
context.save();
var transform = this.getAbsoluteTransform(top);
var m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
context.beginPath();
if (clipFunc) {
clipFunc.call(this, context, this);
} else {
var clipX = this.clipX();
var clipY = this.clipY();
2019-01-02 04:59:27 +08:00
context.rect(clipX, clipY, clipWidth, clipHeight);
}
context.clip();
m = transform.copy().invert().getMatrix();
2019-01-02 04:59:27 +08:00
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
2019-02-19 01:12:03 +08:00
var hasComposition =
!selfCache &&
this.globalCompositeOperation() !== 'source-over' &&
drawMethod === 'drawScene';
if (hasComposition) {
2019-02-19 01:12:03 +08:00
context.save();
context._applyGlobalCompositeOperation(this);
}
this.children.each(function (child) {
child[drawMethod](canvas, top);
2019-01-02 04:59:27 +08:00
});
if (hasComposition) {
2019-02-19 01:12:03 +08:00
context.restore();
}
2019-01-02 04:59:27 +08:00
if (hasClip) {
2019-01-02 04:59:27 +08:00
context.restore();
}
}
2019-08-04 15:38:57 +08:00
2020-03-29 20:45:24 +08:00
getClientRect(config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container<Node>;
}): IRect {
config = config || {};
var skipTransform = config.skipTransform;
var relativeTo = config.relativeTo;
2019-01-02 04:59:27 +08:00
var minX, minY, maxX, maxY;
var selfRect = {
x: Infinity,
y: Infinity,
width: 0,
height: 0,
2019-01-02 04:59:27 +08:00
};
var that = this;
this.children.each(function (child) {
2019-01-02 04:59:27 +08:00
// skip invisible children
2019-04-09 01:17:26 +08:00
if (!child.visible()) {
2019-01-02 04:59:27 +08:00
return;
}
var rect = child.getClientRect({
relativeTo: that,
2020-03-29 20:45:24 +08:00
skipShadow: config.skipShadow,
skipStroke: config.skipStroke,
2019-01-02 04:59:27 +08:00
});
// skip invisible children (like empty groups)
if (rect.width === 0 && rect.height === 0) {
return;
}
if (minX === undefined) {
// initial value for first child
minX = rect.x;
minY = rect.y;
maxX = rect.x + rect.width;
maxY = rect.y + rect.height;
} else {
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
});
// if child is group we need to make sure it has visible shapes inside
var shapes = this.find('Shape');
var hasVisible = false;
for (var i = 0; i < shapes.length; i++) {
var shape = shapes[i];
if (shape._isVisible(this)) {
hasVisible = true;
break;
}
}
if (hasVisible && minX !== undefined) {
2019-01-02 04:59:27 +08:00
selfRect = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
2019-01-02 04:59:27 +08:00
};
} else {
selfRect = {
x: 0,
y: 0,
width: 0,
height: 0,
2019-01-02 04:59:27 +08:00
};
}
if (!skipTransform) {
return this._transformedRect(selfRect, relativeTo);
}
return selfRect;
}
clip: GetSet<IRect, this>;
clipX: GetSet<number, this>;
clipY: GetSet<number, this>;
clipWidth: GetSet<number, this>;
clipHeight: GetSet<number, this>;
2019-08-07 19:45:55 +08:00
// there was "this" instead of "Container<ChildType>",
// but it breaks react-konva types: https://github.com/konvajs/react-konva/issues/390
clipFunc: GetSet<
(ctx: CanvasRenderingContext2D, shape: Container<ChildType>) => void,
this
>;
2019-01-02 04:59:27 +08:00
}
// add getters setters
Factory.addComponentsGetterSetter(Container, 'clip', [
'x',
'y',
'width',
'height',
2019-01-02 04:59:27 +08:00
]);
/**
* get/set clip
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clip
2019-01-02 04:59:27 +08:00
* @param {Object} clip
* @param {Number} clip.x
* @param {Number} clip.y
* @param {Number} clip.width
* @param {Number} clip.height
* @returns {Object}
* @example
* // get clip
* var clip = container.clip();
*
* // set clip
2019-01-06 16:01:20 +08:00
* container.clip({
2019-01-02 04:59:27 +08:00
* x: 20,
* y: 20,
* width: 20,
* height: 20
* });
*/
2019-02-25 01:06:04 +08:00
Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator());
2019-01-02 04:59:27 +08:00
/**
* get/set clip x
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipX
2019-01-02 04:59:27 +08:00
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get clip x
* var clipX = container.clipX();
*
* // set clip x
* container.clipX(10);
*/
2019-02-25 01:06:04 +08:00
Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator());
2019-01-02 04:59:27 +08:00
/**
* get/set clip y
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipY
2019-01-02 04:59:27 +08:00
* @method
* @param {Number} y
* @returns {Number}
* @example
* // get clip y
* var clipY = container.clipY();
*
* // set clip y
* container.clipY(10);
*/
Factory.addGetterSetter(
Container,
'clipWidth',
undefined,
2019-02-25 01:06:04 +08:00
getNumberValidator()
2019-01-02 04:59:27 +08:00
);
/**
* get/set clip width
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipWidth
2019-01-02 04:59:27 +08:00
* @method
* @param {Number} width
* @returns {Number}
* @example
* // get clip width
* var clipWidth = container.clipWidth();
*
* // set clip width
* container.clipWidth(100);
*/
Factory.addGetterSetter(
Container,
'clipHeight',
undefined,
2019-02-25 01:06:04 +08:00
getNumberValidator()
2019-01-02 04:59:27 +08:00
);
/**
* get/set clip height
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipHeight
2019-01-02 04:59:27 +08:00
* @method
* @param {Number} height
* @returns {Number}
* @example
* // get clip height
* var clipHeight = container.clipHeight();
*
* // set clip height
* container.clipHeight(100);
*/
Factory.addGetterSetter(Container, 'clipFunc');
/**
* get/set clip function
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipFunc
2019-01-02 04:59:27 +08:00
* @method
* @param {Function} function
* @returns {Function}
* @example
* // get clip function
* var clipFunction = container.clipFunc();
*
* // set clip height
* container.clipFunc(function(ctx) {
* ctx.rect(0, 0, 100, 100);
* });
*/
Collection.mapMethods(Container);