konva/src/Stage.ts

1099 lines
30 KiB
TypeScript
Raw Normal View History

2019-01-02 04:59:27 +08:00
import { Util, Collection } from './Util';
2019-01-11 21:51:46 +08:00
import { Factory } from './Factory';
import { Container, ContainerConfig } from './Container';
2019-03-07 11:19:32 +08:00
import { Konva } from './Global';
2019-01-02 04:59:27 +08:00
import { SceneCanvas, HitCanvas } from './Canvas';
import { GetSet, Vector2d } from './types';
import { Shape } from './Shape';
import { Layer } from './Layer';
2019-02-24 10:36:05 +08:00
import { DD } from './DragAndDrop';
2019-02-27 21:06:04 +08:00
import { _registerNode } from './Global';
2019-03-27 04:30:00 +08:00
import * as PointerEvents from './PointerEvents';
2019-01-02 04:59:27 +08:00
export interface StageConfig extends ContainerConfig {
container: HTMLDivElement | string;
}
2019-01-02 04:59:27 +08:00
// CONSTANTS
var STAGE = 'Stage',
STRING = 'string',
PX = 'px',
MOUSEOUT = 'mouseout',
MOUSELEAVE = 'mouseleave',
MOUSEOVER = 'mouseover',
MOUSEENTER = 'mouseenter',
MOUSEMOVE = 'mousemove',
MOUSEDOWN = 'mousedown',
MOUSEUP = 'mouseup',
// TODO: add them into "on" method docs and into site docs
2019-03-27 04:30:00 +08:00
POINTERMOVE = 'pointermove',
POINTERDOWN = 'pointerdown',
POINTERUP = 'pointerup',
POINTERCANCEL = 'pointercancel',
LOSTPOINTERCAPTURE = 'lostpointercapture',
2019-01-02 04:59:27 +08:00
CONTEXTMENU = 'contextmenu',
CLICK = 'click',
DBL_CLICK = 'dblclick',
TOUCHSTART = 'touchstart',
TOUCHEND = 'touchend',
TAP = 'tap',
DBL_TAP = 'dbltap',
TOUCHMOVE = 'touchmove',
WHEEL = 'wheel',
CONTENT_MOUSEOUT = 'contentMouseout',
CONTENT_MOUSEOVER = 'contentMouseover',
CONTENT_MOUSEMOVE = 'contentMousemove',
CONTENT_MOUSEDOWN = 'contentMousedown',
CONTENT_MOUSEUP = 'contentMouseup',
CONTENT_CONTEXTMENU = 'contentContextmenu',
CONTENT_CLICK = 'contentClick',
CONTENT_DBL_CLICK = 'contentDblclick',
CONTENT_TOUCHSTART = 'contentTouchstart',
CONTENT_TOUCHEND = 'contentTouchend',
CONTENT_DBL_TAP = 'contentDbltap',
CONTENT_TAP = 'contentTap',
CONTENT_TOUCHMOVE = 'contentTouchmove',
2019-04-10 22:28:42 +08:00
CONTENT_POINTERMOVE = 'contentPointermove',
CONTENT_POINTERDOWN = 'contentPointerdown',
CONTENT_POINTERUP = 'contentPointerup',
2019-01-02 04:59:27 +08:00
CONTENT_WHEEL = 'contentWheel',
RELATIVE = 'relative',
KONVA_CONTENT = 'konvajs-content',
SPACE = ' ',
UNDERSCORE = '_',
CONTAINER = 'container',
MAX_LAYERS_NUMBER = 5,
2019-01-02 04:59:27 +08:00
EMPTY_STRING = '',
EVENTS = [
2019-04-04 09:28:48 +08:00
MOUSEENTER,
2019-01-02 04:59:27 +08:00
MOUSEDOWN,
MOUSEMOVE,
MOUSEUP,
MOUSEOUT,
TOUCHSTART,
TOUCHMOVE,
TOUCHEND,
MOUSEOVER,
WHEEL,
2019-03-27 04:30:00 +08:00
CONTEXTMENU,
POINTERDOWN,
POINTERMOVE,
POINTERUP,
POINTERCANCEL,
LOSTPOINTERCAPTURE,
2019-01-02 04:59:27 +08:00
],
// cached variables
eventsLength = EVENTS.length;
function addEvent(ctx, eventName) {
ctx.content.addEventListener(
eventName,
function (evt) {
2019-01-02 04:59:27 +08:00
ctx[UNDERSCORE + eventName](evt);
},
false
);
}
const NO_POINTERS_MESSAGE = `Pointer position is missing and not registered by the stage. Looks like it is outside of the stage container. You can set it manually from event: stage.setPointersPositions(event);`;
2019-01-02 04:59:27 +08:00
export const stages: Stage[] = [];
function checkNoClip(attrs: any = {}) {
if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) {
Util.warn(
'Stage does not support clipping. Please use clip for Layers or Groups.'
);
}
return attrs;
}
2019-01-02 04:59:27 +08:00
/**
* Stage constructor. A stage is used to contain multiple layers
* @constructor
* @memberof Konva
* @augments Konva.Container
* @param {Object} config
* @param {String|Element} config.container Container selector or DOM element
* @@nodeParams
* @example
* var stage = new Konva.Stage({
* width: 500,
* height: 800,
* container: 'containerId' // or "#containerId" or ".containerClass"
* });
*/
export class Stage extends Container<Layer> {
2019-01-02 04:59:27 +08:00
content: HTMLDivElement;
pointerPos: Vector2d | null;
2019-08-04 15:38:57 +08:00
_pointerPositions: (Vector2d & { id?: number })[] = [];
_changedPointerPositions: (Vector2d & { id?: number })[] = [];
2019-08-04 10:41:57 +08:00
2019-01-02 04:59:27 +08:00
bufferCanvas: SceneCanvas;
bufferHitCanvas: HitCanvas;
targetShape: Shape;
clickStartShape: Shape;
clickEndShape: Shape;
tapStartShape: Shape;
2020-07-01 02:04:06 +08:00
tapEndShape: Shape;
dblTimeout: any;
2019-01-02 04:59:27 +08:00
constructor(config: StageConfig) {
super(checkNoClip(config));
2019-01-02 04:59:27 +08:00
this._buildDOM();
this._bindContentEvents();
stages.push(this);
this.on('widthChange.konva heightChange.konva', this._resizeDOM);
this.on('visibleChange.konva', this._checkVisibility);
this.on(
'clipWidthChange.konva clipHeightChange.konva clipFuncChange.konva',
() => {
checkNoClip(this.attrs);
}
);
this._checkVisibility();
2019-01-02 04:59:27 +08:00
}
_validateAdd(child) {
2019-02-27 21:06:04 +08:00
const isLayer = child.getType() === 'Layer';
const isFastLayer = child.getType() === 'FastLayer';
const valid = isLayer || isFastLayer;
if (!valid) {
2019-01-02 04:59:27 +08:00
Util.throw('You may only add layers to the stage.');
}
}
_checkVisibility() {
2020-03-15 10:07:37 +08:00
if (!this.content) {
return;
}
const style = this.visible() ? '' : 'none';
this.content.style.display = style;
}
2019-01-02 04:59:27 +08:00
/**
* set container dom element which contains the stage wrapper div element
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#setContainer
2019-01-02 04:59:27 +08:00
* @param {DomElement} container can pass in a dom element or id string
*/
setContainer(container) {
if (typeof container === STRING) {
if (container.charAt(0) === '.') {
var className = container.slice(1);
container = document.getElementsByClassName(className)[0];
} else {
var id;
if (container.charAt(0) !== '#') {
id = container;
} else {
id = container.slice(1);
}
container = document.getElementById(id);
}
if (!container) {
throw 'Can not find container in document with id ' + id;
}
}
this._setAttr(CONTAINER, container);
if (this.content) {
if (this.content.parentElement) {
this.content.parentElement.removeChild(this.content);
}
container.appendChild(this.content);
}
2019-01-02 04:59:27 +08:00
return this;
}
shouldDrawHit() {
return true;
}
/**
* clear all layers
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#clear
2019-01-02 04:59:27 +08:00
*/
clear() {
var layers = this.children,
len = layers.length,
n;
for (n = 0; n < len; n++) {
layers[n].clear();
}
return this;
}
clone(obj) {
if (!obj) {
obj = {};
}
2019-03-07 11:19:32 +08:00
obj.container = document.createElement('div');
2019-01-02 04:59:27 +08:00
return Container.prototype.clone.call(this, obj);
}
2019-01-06 16:01:20 +08:00
2019-01-02 04:59:27 +08:00
destroy() {
2019-01-06 16:01:20 +08:00
super.destroy();
2019-01-02 04:59:27 +08:00
2019-01-06 16:01:20 +08:00
var content = this.content;
2019-01-02 04:59:27 +08:00
if (content && Util._isInDocument(content)) {
this.container().removeChild(content);
}
var index = stages.indexOf(this);
if (index > -1) {
stages.splice(index, 1);
}
return this;
}
/**
* returns absolute pointer position which can be a touch position or mouse position
* pointer position doesn't include any transforms (such as scale) of the stage
* it is just a plain position of pointer relative to top-left corner of the stage container
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#getPointerPosition
2019-08-22 22:40:05 +08:00
* @returns {Vector2d|null}
2019-01-02 04:59:27 +08:00
*/
2019-08-22 22:40:05 +08:00
getPointerPosition(): Vector2d | null {
const pos = this._pointerPositions[0] || this._changedPointerPositions[0];
2019-08-04 10:41:57 +08:00
if (!pos) {
Util.warn(NO_POINTERS_MESSAGE);
2019-08-22 22:40:05 +08:00
return null;
}
2019-08-04 15:38:57 +08:00
return {
x: pos.x,
y: pos.y,
2019-08-04 15:38:57 +08:00
};
}
_getPointerById(id?: number) {
return this._pointerPositions.find((p) => p.id === id);
2019-08-04 10:41:57 +08:00
}
getPointersPositions() {
return this._pointerPositions;
2019-01-02 04:59:27 +08:00
}
getStage() {
return this;
}
getContent() {
return this.content;
}
_toKonvaCanvas(config) {
config = config || {};
config.x = config.x || 0;
config.y = config.y || 0;
config.width = config.width || this.width();
config.height = config.height || this.height();
2019-01-02 04:59:27 +08:00
var canvas = new SceneCanvas({
width: config.width,
height: config.height,
pixelRatio: config.pixelRatio || 1,
});
var _context = canvas.getContext()._context;
var layers = this.children;
if (config.x || config.y) {
_context.translate(-1 * config.x, -1 * config.y);
2019-01-02 04:59:27 +08:00
}
layers.each(function (layer) {
2019-01-02 04:59:27 +08:00
if (!layer.isVisible()) {
return;
}
var layerCanvas = layer._toKonvaCanvas(config);
_context.drawImage(
layerCanvas._canvas,
config.x,
config.y,
2019-01-02 04:59:27 +08:00
layerCanvas.getWidth() / layerCanvas.getPixelRatio(),
layerCanvas.getHeight() / layerCanvas.getPixelRatio()
);
});
return canvas;
}
/**
* get visible intersection shape. This is the preferred
* method for determining if a point intersects a shape or not
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#getIntersection
2019-01-02 04:59:27 +08:00
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @param {String} [selector]
* @returns {Konva.Node}
* @example
* var shape = stage.getIntersection({x: 50, y: 50});
* // or if you interested in shape parent:
* var group = stage.getIntersection({x: 50, y: 50}, 'Group');
*/
2019-08-22 22:40:05 +08:00
getIntersection(pos: Vector2d | null, selector?: string): Shape | null {
if (!pos) {
return null;
}
2019-01-02 04:59:27 +08:00
var layers = this.children,
len = layers.length,
end = len - 1,
n,
shape;
for (n = end; n >= 0; n--) {
shape = layers[n].getIntersection(pos, selector);
if (shape) {
return shape;
}
}
return null;
}
_resizeDOM() {
2020-03-15 10:07:37 +08:00
var width = this.width();
var height = this.height();
2019-01-02 04:59:27 +08:00
if (this.content) {
// set content dimensions
this.content.style.width = width + PX;
this.content.style.height = height + PX;
2020-03-15 10:07:37 +08:00
}
2019-01-02 04:59:27 +08:00
2020-03-15 10:07:37 +08:00
this.bufferCanvas.setSize(width, height);
this.bufferHitCanvas.setSize(width, height);
2019-01-02 04:59:27 +08:00
2020-03-15 10:07:37 +08:00
// set layer dimensions
this.children.each((layer) => {
2020-03-15 10:07:37 +08:00
layer.setSize({ width, height });
layer.draw();
});
2019-01-02 04:59:27 +08:00
}
2020-10-16 14:34:14 +08:00
add(layer: Layer) {
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;
}
2019-01-06 16:01:20 +08:00
super.add(layer);
var length = this.children.length;
if (length > MAX_LAYERS_NUMBER) {
Util.warn(
'The stage has ' +
length +
2019-11-17 09:35:58 +08:00
' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'
);
}
layer.setSize({ width: this.width(), height: this.height() });
2019-01-02 04:59:27 +08:00
// draw layer and append canvas to container
layer.draw();
2019-03-07 11:19:32 +08:00
if (Konva.isBrowser) {
2019-01-02 04:59:27 +08:00
this.content.appendChild(layer.canvas._canvas);
}
// chainable
return this;
}
getParent() {
return null;
}
getLayer() {
return null;
}
hasPointerCapture(pointerId: number): boolean {
return PointerEvents.hasPointerCapture(pointerId, this);
}
setPointerCapture(pointerId: number) {
PointerEvents.setPointerCapture(pointerId, this);
}
releaseCapture(pointerId: number) {
PointerEvents.releaseCapture(pointerId, this);
}
2019-01-02 04:59:27 +08:00
/**
* returns a {@link Konva.Collection} of layers
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#getLayers
2019-01-02 04:59:27 +08:00
*/
getLayers() {
return this.getChildren();
}
_bindContentEvents() {
2019-03-07 11:19:32 +08:00
if (!Konva.isBrowser) {
2019-01-02 04:59:27 +08:00
return;
}
for (var n = 0; n < eventsLength; n++) {
addEvent(this, EVENTS[n]);
}
}
2019-04-04 09:28:48 +08:00
_mouseenter(evt) {
this.setPointersPositions(evt);
this._fire(MOUSEENTER, { evt: evt, target: this, currentTarget: this });
}
2019-01-02 04:59:27 +08:00
_mouseover(evt) {
this.setPointersPositions(evt);
2019-01-02 04:59:27 +08:00
this._fire(CONTENT_MOUSEOVER, { evt: evt });
this._fire(MOUSEOVER, { evt: evt, target: this, currentTarget: this });
2019-01-02 04:59:27 +08:00
}
_mouseout(evt) {
this.setPointersPositions(evt);
var targetShape = this.targetShape?.getStage() ? this.targetShape : null;
2019-01-02 04:59:27 +08:00
2019-08-08 17:24:55 +08:00
var eventsEnabled = !DD.isDragging || Konva.hitOnDragEnabled;
if (targetShape && eventsEnabled) {
2019-01-02 04:59:27 +08:00
targetShape._fireAndBubble(MOUSEOUT, { evt: evt });
targetShape._fireAndBubble(MOUSELEAVE, { evt: evt });
2019-12-19 02:16:03 +08:00
this._fire(MOUSELEAVE, { evt: evt, target: this, currentTarget: this });
2019-01-02 04:59:27 +08:00
this.targetShape = null;
2019-08-08 17:24:55 +08:00
} else if (eventsEnabled) {
2019-04-04 09:28:48 +08:00
this._fire(MOUSELEAVE, {
evt: evt,
target: this,
currentTarget: this,
2019-04-04 09:28:48 +08:00
});
this._fire(MOUSEOUT, {
evt: evt,
target: this,
currentTarget: this,
2019-04-04 09:28:48 +08:00
});
2019-01-02 04:59:27 +08:00
}
this.pointerPos = undefined;
2019-08-04 10:41:57 +08:00
this._pointerPositions = [];
2019-01-02 04:59:27 +08:00
this._fire(CONTENT_MOUSEOUT, { evt: evt });
}
_mousemove(evt) {
// workaround for mobile IE to force touch event when unhandled pointer event elevates into a mouse event
2019-03-07 11:19:32 +08:00
if (Konva.UA.ieMobile) {
2019-01-02 04:59:27 +08:00
return this._touchmove(evt);
}
this.setPointersPositions(evt);
2019-08-04 15:38:57 +08:00
var pointerId = Util._getFirstPointerId(evt);
2019-04-04 09:28:48 +08:00
var shape: Shape;
var targetShape = this.targetShape?.getStage() ? this.targetShape : null;
2019-08-08 17:24:55 +08:00
var eventsEnabled = !DD.isDragging || Konva.hitOnDragEnabled;
if (eventsEnabled) {
2019-01-02 04:59:27 +08:00
shape = this.getIntersection(this.getPointerPosition());
if (shape && shape.isListening()) {
var differentTarget = targetShape !== shape;
2019-08-08 17:24:55 +08:00
if (eventsEnabled && differentTarget) {
if (targetShape) {
targetShape._fireAndBubble(
2019-08-04 15:38:57 +08:00
MOUSEOUT,
{ evt: evt, pointerId },
shape
);
targetShape._fireAndBubble(
2019-08-04 15:38:57 +08:00
MOUSELEAVE,
{ evt: evt, pointerId },
shape
);
2019-01-02 04:59:27 +08:00
}
shape._fireAndBubble(MOUSEOVER, { evt: evt, pointerId }, targetShape);
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(
MOUSEENTER,
{ evt: evt, pointerId },
targetShape
2019-08-04 15:38:57 +08:00
);
2019-08-08 17:24:55 +08:00
shape._fireAndBubble(MOUSEMOVE, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
this.targetShape = shape;
} else {
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(MOUSEMOVE, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
}
} else {
/*
* if no shape was detected, clear target shape and try
* to run mouseout from previous target shape
*/
if (targetShape && eventsEnabled) {
targetShape._fireAndBubble(MOUSEOUT, { evt: evt, pointerId });
targetShape._fireAndBubble(MOUSELEAVE, { evt: evt, pointerId });
this._fire(MOUSEOVER, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId,
});
2019-01-02 04:59:27 +08:00
this.targetShape = null;
}
this._fire(MOUSEMOVE, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId,
2019-01-02 04:59:27 +08:00
});
}
// content event
this._fire(CONTENT_MOUSEMOVE, { evt: evt });
}
// always call preventDefault for desktop events because some browsers
// try to drag and drop the canvas element
if (evt.cancelable) {
evt.preventDefault();
}
}
_mousedown(evt) {
// workaround for mobile IE to force touch event when unhandled pointer event elevates into a mouse event
2019-03-07 11:19:32 +08:00
if (Konva.UA.ieMobile) {
2019-01-02 04:59:27 +08:00
return this._touchstart(evt);
}
this.setPointersPositions(evt);
2019-08-04 15:38:57 +08:00
var pointerId = Util._getFirstPointerId(evt);
2019-01-02 04:59:27 +08:00
var shape = this.getIntersection(this.getPointerPosition());
DD.justDragged = false;
2019-03-07 11:19:32 +08:00
Konva.listenClickTap = true;
2019-01-02 04:59:27 +08:00
if (shape && shape.isListening()) {
this.clickStartShape = shape;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(MOUSEDOWN, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
} else {
this._fire(MOUSEDOWN, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId,
2019-01-02 04:59:27 +08:00
});
}
// content event
this._fire(CONTENT_MOUSEDOWN, { evt: evt });
// Do not prevent default behavior, because it will prevent listening events outside of window iframe
// we used preventDefault for disabling native drag&drop
// but userSelect = none style will do the trick
2019-01-02 04:59:27 +08:00
// if (evt.cancelable) {
// evt.preventDefault();
2019-01-02 04:59:27 +08:00
// }
}
_mouseup(evt) {
// workaround for mobile IE to force touch event when unhandled pointer event elevates into a mouse event
2019-03-07 11:19:32 +08:00
if (Konva.UA.ieMobile) {
2019-01-02 04:59:27 +08:00
return this._touchend(evt);
}
this.setPointersPositions(evt);
2019-08-04 15:38:57 +08:00
var pointerId = Util._getFirstPointerId(evt);
2019-01-02 04:59:27 +08:00
var shape = this.getIntersection(this.getPointerPosition()),
clickStartShape = this.clickStartShape,
clickEndShape = this.clickEndShape,
2019-03-07 11:19:32 +08:00
fireDblClick = false;
2019-01-02 04:59:27 +08:00
2019-03-07 11:19:32 +08:00
if (Konva.inDblClickWindow) {
2019-01-02 04:59:27 +08:00
fireDblClick = true;
clearTimeout(this.dblTimeout);
// Konva.inDblClickWindow = false;
2019-03-07 11:19:32 +08:00
} else if (!DD.justDragged) {
2019-01-02 04:59:27 +08:00
// don't set inDblClickWindow after dragging
2019-03-07 11:19:32 +08:00
Konva.inDblClickWindow = true;
2019-01-02 04:59:27 +08:00
clearTimeout(this.dblTimeout);
}
this.dblTimeout = setTimeout(function () {
2019-03-07 11:19:32 +08:00
Konva.inDblClickWindow = false;
}, Konva.dblClickWindow);
2019-01-02 04:59:27 +08:00
if (shape && shape.isListening()) {
this.clickEndShape = shape;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(MOUSEUP, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
// detect if click or double click occurred
if (
2019-03-07 11:19:32 +08:00
Konva.listenClickTap &&
2019-01-02 04:59:27 +08:00
clickStartShape &&
clickStartShape._id === shape._id
) {
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(CLICK, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
2019-07-18 08:55:22 +08:00
if (fireDblClick && clickEndShape && clickEndShape === shape) {
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(DBL_CLICK, { evt: evt, pointerId });
2019-01-02 04:59:27 +08:00
}
}
} else {
this.clickEndShape = null;
2019-08-04 15:38:57 +08:00
this._fire(MOUSEUP, {
evt: evt,
target: this,
currentTarget: this,
pointerId,
2019-08-04 15:38:57 +08:00
});
2019-03-07 11:19:32 +08:00
if (Konva.listenClickTap) {
2019-08-04 15:38:57 +08:00
this._fire(CLICK, {
evt: evt,
target: this,
currentTarget: this,
pointerId,
2019-08-04 15:38:57 +08:00
});
2019-01-02 04:59:27 +08:00
}
if (fireDblClick) {
this._fire(DBL_CLICK, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId,
2019-01-02 04:59:27 +08:00
});
}
}
// content events
this._fire(CONTENT_MOUSEUP, { evt: evt });
2019-03-07 11:19:32 +08:00
if (Konva.listenClickTap) {
2019-01-02 04:59:27 +08:00
this._fire(CONTENT_CLICK, { evt: evt });
if (fireDblClick) {
this._fire(CONTENT_DBL_CLICK, { evt: evt });
}
}
2019-03-07 11:19:32 +08:00
Konva.listenClickTap = false;
2019-01-02 04:59:27 +08:00
// always call preventDefault for desktop events because some browsers
// try to drag and drop the canvas element
if (evt.cancelable) {
evt.preventDefault();
}
}
_contextmenu(evt) {
this.setPointersPositions(evt);
2019-01-02 04:59:27 +08:00
var shape = this.getIntersection(this.getPointerPosition());
if (shape && shape.isListening()) {
shape._fireAndBubble(CONTEXTMENU, { evt: evt });
} else {
this._fire(CONTEXTMENU, {
evt: evt,
target: this,
currentTarget: this,
2019-01-02 04:59:27 +08:00
});
}
this._fire(CONTENT_CONTEXTMENU, { evt: evt });
}
_touchstart(evt) {
this.setPointersPositions(evt);
2019-08-04 10:41:57 +08:00
var triggeredOnShape = false;
this._changedPointerPositions.forEach((pos) => {
2019-08-04 10:41:57 +08:00
var shape = this.getIntersection(pos);
Konva.listenClickTap = true;
DD.justDragged = false;
2019-08-04 10:41:57 +08:00
const hasShape = shape && shape.isListening();
2019-01-02 04:59:27 +08:00
2019-08-04 10:41:57 +08:00
if (!hasShape) {
return;
}
2019-01-02 04:59:27 +08:00
2019-08-04 10:41:57 +08:00
if (Konva.captureTouchEventsEnabled) {
shape.setPointerCapture(pos.id);
}
2019-01-02 04:59:27 +08:00
2019-08-04 10:41:57 +08:00
this.tapStartShape = shape;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(TOUCHSTART, { evt: evt, pointerId: pos.id }, this);
2019-08-04 10:41:57 +08:00
triggeredOnShape = true;
2019-01-02 04:59:27 +08:00
// only call preventDefault if the shape is listening for events
if (shape.isListening() && shape.preventDefault() && evt.cancelable) {
evt.preventDefault();
}
2019-08-04 10:41:57 +08:00
});
if (!triggeredOnShape) {
2019-01-02 04:59:27 +08:00
this._fire(TOUCHSTART, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
2019-01-02 04:59:27 +08:00
});
}
2019-08-04 10:41:57 +08:00
2019-01-02 04:59:27 +08:00
// content event
this._fire(CONTENT_TOUCHSTART, { evt: evt });
}
2019-08-04 10:41:57 +08:00
_touchmove(evt) {
this.setPointersPositions(evt);
2019-08-08 17:24:55 +08:00
var eventsEnabled = !DD.isDragging || Konva.hitOnDragEnabled;
if (eventsEnabled) {
2019-08-04 10:41:57 +08:00
var triggeredOnShape = false;
var processedShapesIds = {};
this._changedPointerPositions.forEach((pos) => {
2019-08-04 10:41:57 +08:00
const shape =
PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos);
const hasShape = shape && shape.isListening();
if (!hasShape) {
return;
}
if (processedShapesIds[shape._id]) {
return;
}
processedShapesIds[shape._id] = true;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(TOUCHMOVE, { evt: evt, pointerId: pos.id });
2019-08-04 10:41:57 +08:00
triggeredOnShape = true;
// only call preventDefault if the shape is listening for events
if (shape.isListening() && shape.preventDefault() && evt.cancelable) {
evt.preventDefault();
}
});
if (!triggeredOnShape) {
this._fire(TOUCHMOVE, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
2019-08-04 10:41:57 +08:00
});
}
this._fire(CONTENT_TOUCHMOVE, { evt: evt });
}
if (DD.isDragging && DD.node.preventDefault() && evt.cancelable) {
evt.preventDefault();
}
}
2019-01-02 04:59:27 +08:00
_touchend(evt) {
this.setPointersPositions(evt);
2019-08-04 10:41:57 +08:00
2020-07-01 02:04:06 +08:00
var tapEndShape = this.tapEndShape,
2019-01-02 04:59:27 +08:00
fireDblClick = false;
2019-03-07 11:19:32 +08:00
if (Konva.inDblClickWindow) {
2019-01-02 04:59:27 +08:00
fireDblClick = true;
clearTimeout(this.dblTimeout);
2019-03-07 11:19:32 +08:00
// Konva.inDblClickWindow = false;
2019-09-03 22:38:19 +08:00
} else if (!DD.justDragged) {
2019-03-07 11:19:32 +08:00
Konva.inDblClickWindow = true;
2019-01-02 04:59:27 +08:00
clearTimeout(this.dblTimeout);
}
this.dblTimeout = setTimeout(function () {
2019-03-07 11:19:32 +08:00
Konva.inDblClickWindow = false;
}, Konva.dblClickWindow);
2019-01-02 04:59:27 +08:00
2019-08-04 10:41:57 +08:00
var triggeredOnShape = false;
var processedShapesIds = {};
2019-08-17 13:47:48 +08:00
var tapTriggered = false;
var dblTapTriggered = false;
this._changedPointerPositions.forEach((pos) => {
2019-08-04 10:41:57 +08:00
var shape =
(PointerEvents.getCapturedShape(pos.id) as Shape) ||
this.getIntersection(pos);
if (shape) {
shape.releaseCapture(pos.id);
}
const hasShape = shape && shape.isListening();
if (!hasShape) {
return;
}
if (processedShapesIds[shape._id]) {
return;
}
processedShapesIds[shape._id] = true;
2020-07-01 02:04:06 +08:00
this.tapEndShape = shape;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(TOUCHEND, { evt: evt, pointerId: pos.id });
2019-08-04 10:41:57 +08:00
triggeredOnShape = true;
2019-01-02 04:59:27 +08:00
// detect if tap or double tap occurred
2019-08-17 13:47:48 +08:00
if (Konva.listenClickTap && shape === this.tapStartShape) {
tapTriggered = true;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(TAP, { evt: evt, pointerId: pos.id });
2019-01-02 04:59:27 +08:00
2020-07-01 02:04:06 +08:00
if (fireDblClick && tapEndShape && tapEndShape === shape) {
2019-08-17 13:47:48 +08:00
dblTapTriggered = true;
2019-08-04 15:38:57 +08:00
shape._fireAndBubble(DBL_TAP, { evt: evt, pointerId: pos.id });
2019-01-02 04:59:27 +08:00
}
}
2019-08-04 10:41:57 +08:00
2019-01-02 04:59:27 +08:00
// only call preventDefault if the shape is listening for events
if (shape.isListening() && shape.preventDefault() && evt.cancelable) {
evt.preventDefault();
}
2019-08-04 10:41:57 +08:00
});
if (!triggeredOnShape) {
2019-08-04 15:38:57 +08:00
this._fire(TOUCHEND, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
2019-08-04 15:38:57 +08:00
});
2019-08-04 10:41:57 +08:00
}
2019-08-17 13:47:48 +08:00
if (Konva.listenClickTap && !tapTriggered) {
2020-07-01 02:04:06 +08:00
this.tapEndShape = null;
2019-08-04 15:38:57 +08:00
this._fire(TAP, {
evt: evt,
target: this,
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
2019-08-04 15:38:57 +08:00
});
2019-08-04 10:41:57 +08:00
}
2019-08-17 13:47:48 +08:00
if (fireDblClick && !dblTapTriggered) {
2019-08-04 10:41:57 +08:00
this._fire(DBL_TAP, {
evt: evt,
target: this,
2019-08-04 15:38:57 +08:00
currentTarget: this,
pointerId: this._changedPointerPositions[0].id,
2019-08-04 10:41:57 +08:00
});
2019-01-02 04:59:27 +08:00
}
// content events
this._fire(CONTENT_TOUCHEND, { evt: evt });
2019-03-07 11:19:32 +08:00
if (Konva.listenClickTap) {
2019-01-02 04:59:27 +08:00
this._fire(CONTENT_TAP, { evt: evt });
if (fireDblClick) {
this._fire(CONTENT_DBL_TAP, { evt: evt });
}
}
2020-07-01 02:04:06 +08:00
if (this.preventDefault() && evt.cancelable) {
evt.preventDefault();
}
2019-03-07 11:19:32 +08:00
Konva.listenClickTap = false;
2019-01-02 04:59:27 +08:00
}
2019-08-04 10:41:57 +08:00
2019-01-02 04:59:27 +08:00
_wheel(evt) {
this.setPointersPositions(evt);
2019-01-02 04:59:27 +08:00
var shape = this.getIntersection(this.getPointerPosition());
if (shape && shape.isListening()) {
shape._fireAndBubble(WHEEL, { evt: evt });
} else {
this._fire(WHEEL, {
evt: evt,
target: this,
currentTarget: this,
2019-01-02 04:59:27 +08:00
});
}
this._fire(CONTENT_WHEEL, { evt: evt });
}
2019-03-27 04:30:00 +08:00
_pointerdown(evt: PointerEvent) {
if (!Konva._pointerEventsEnabled) {
return;
}
2019-03-27 04:30:00 +08:00
this.setPointersPositions(evt);
const shape =
PointerEvents.getCapturedShape(evt.pointerId) ||
this.getIntersection(this.getPointerPosition());
if (shape) {
shape._fireAndBubble(POINTERDOWN, PointerEvents.createEvent(evt));
}
}
_pointermove(evt: PointerEvent) {
if (!Konva._pointerEventsEnabled) {
return;
}
2019-03-27 04:30:00 +08:00
this.setPointersPositions(evt);
const shape =
PointerEvents.getCapturedShape(evt.pointerId) ||
this.getIntersection(this.getPointerPosition());
if (shape) {
shape._fireAndBubble(POINTERMOVE, PointerEvents.createEvent(evt));
}
}
_pointerup(evt: PointerEvent) {
if (!Konva._pointerEventsEnabled) {
return;
}
2019-03-27 04:30:00 +08:00
this.setPointersPositions(evt);
const shape =
PointerEvents.getCapturedShape(evt.pointerId) ||
this.getIntersection(this.getPointerPosition());
if (shape) {
shape._fireAndBubble(POINTERUP, PointerEvents.createEvent(evt));
}
PointerEvents.releaseCapture(evt.pointerId);
}
_pointercancel(evt: PointerEvent) {
if (!Konva._pointerEventsEnabled) {
return;
}
2019-03-27 04:30:00 +08:00
this.setPointersPositions(evt);
const shape =
PointerEvents.getCapturedShape(evt.pointerId) ||
this.getIntersection(this.getPointerPosition());
if (shape) {
shape._fireAndBubble(POINTERUP, PointerEvents.createEvent(evt));
}
PointerEvents.releaseCapture(evt.pointerId);
}
_lostpointercapture(evt: PointerEvent) {
PointerEvents.releaseCapture(evt.pointerId);
}
/**
* manually register pointers positions (mouse/touch) in the stage.
* So you can use stage.getPointerPosition(). Usually you don't need to use that method
* because all internal events are automatically registered. It may be useful if event
* is triggered outside of the stage, but you still want to use Konva methods to get pointers position.
* @method
* @name Konva.Stage#setPointersPositions
* @param {Object} event Event object
* @example
*
* window.addEventListener('mousemove', (e) => {
* stage.setPointersPositions(e);
* });
*/
setPointersPositions(evt) {
2019-01-02 04:59:27 +08:00
var contentPosition = this._getContentPosition(),
x = null,
y = null;
evt = evt ? evt : window.event;
// touch events
if (evt.touches !== undefined) {
2019-08-04 10:41:57 +08:00
// touchlist has not support for map method
// so we have to iterate
this._pointerPositions = [];
this._changedPointerPositions = [];
Collection.prototype.each.call(evt.touches, (touch: any) => {
this._pointerPositions.push({
id: touch.identifier,
x: (touch.clientX - contentPosition.left) / contentPosition.scaleX,
y: (touch.clientY - contentPosition.top) / contentPosition.scaleY,
2019-08-04 10:41:57 +08:00
});
});
Collection.prototype.each.call(
evt.changedTouches || evt.touches,
(touch: any) => {
this._changedPointerPositions.push({
id: touch.identifier,
x: (touch.clientX - contentPosition.left) / contentPosition.scaleX,
y: (touch.clientY - contentPosition.top) / contentPosition.scaleY,
2019-08-04 10:41:57 +08:00
});
}
);
2019-01-02 04:59:27 +08:00
} else {
// mouse events
x = (evt.clientX - contentPosition.left) / contentPosition.scaleX;
y = (evt.clientY - contentPosition.top) / contentPosition.scaleY;
2019-01-02 04:59:27 +08:00
this.pointerPos = {
x: x,
y: y,
2019-01-02 04:59:27 +08:00
};
2019-08-04 15:38:57 +08:00
this._pointerPositions = [{ x, y, id: Util._getFirstPointerId(evt) }];
this._changedPointerPositions = [
{ x, y, id: Util._getFirstPointerId(evt) },
2019-08-04 15:38:57 +08:00
];
2019-01-02 04:59:27 +08:00
}
}
_setPointerPosition(evt) {
Util.warn(
'Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'
);
this.setPointersPositions(evt);
}
2019-01-02 04:59:27 +08:00
_getContentPosition() {
2020-03-15 10:07:37 +08:00
if (!this.content || !this.content.getBoundingClientRect) {
return {
top: 0,
left: 0,
scaleX: 1,
scaleY: 1,
2020-03-15 10:07:37 +08:00
};
}
var rect = this.content.getBoundingClientRect();
2019-01-02 04:59:27 +08:00
return {
top: rect.top,
left: rect.left,
2020-01-08 21:15:12 +08:00
// sometimes clientWidth can be equals to 0
// i saw it in react-konva test, looks like it is because of hidden testing element
scaleX: rect.width / this.content.clientWidth || 1,
scaleY: rect.height / this.content.clientHeight || 1,
2019-01-02 04:59:27 +08:00
};
}
_buildDOM() {
2020-03-15 10:07:37 +08:00
this.bufferCanvas = new SceneCanvas({
width: this.width(),
height: this.height(),
2020-03-15 10:07:37 +08:00
});
this.bufferHitCanvas = new HitCanvas({
pixelRatio: 1,
width: this.width(),
height: this.height(),
2020-03-15 10:07:37 +08:00
});
2019-01-02 04:59:27 +08:00
2019-03-07 11:19:32 +08:00
if (!Konva.isBrowser) {
2019-01-02 04:59:27 +08:00
return;
}
var container = this.container();
if (!container) {
throw 'Stage has no container. A container is required.';
}
// clear content inside container
container.innerHTML = EMPTY_STRING;
// content
2019-03-07 11:19:32 +08:00
this.content = document.createElement('div');
2019-01-02 04:59:27 +08:00
this.content.style.position = RELATIVE;
this.content.style.userSelect = 'none';
this.content.className = KONVA_CONTENT;
this.content.setAttribute('role', 'presentation');
container.appendChild(this.content);
this._resizeDOM();
}
// currently cache function is now working for stage, because stage has no its own canvas element
cache() {
Util.warn(
'Cache function is not allowed for stage. You may use cache only for layers, groups and shapes.'
);
return this;
}
clearCache() {
return this;
}
2019-01-25 13:20:15 +08:00
/**
* batch draw
* @method
2020-11-10 21:59:20 +08:00
* @name Konva.Stage#batchDraw
2019-01-25 13:20:15 +08:00
* @return {Konva.Stage} this
*/
batchDraw() {
this.children.each(function (layer) {
2019-01-25 13:20:15 +08:00
layer.batchDraw();
});
return this;
}
2019-01-02 04:59:27 +08:00
container: GetSet<HTMLDivElement, this>;
}
Stage.prototype.nodeType = STAGE;
2019-02-27 21:06:04 +08:00
_registerNode(Stage);
2019-01-02 04:59:27 +08:00
/**
2019-01-06 16:01:20 +08:00
* get/set container DOM element
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Stage#container
2019-01-02 04:59:27 +08:00
* @returns {DomElement} container
* @example
* // get container
* var container = stage.container();
* // set container
* var container = document.createElement('div');
* body.appendChild(container);
* stage.container(container);
*/
2019-02-19 01:12:03 +08:00
Factory.addGetterSetter(Stage, 'container');