UNPKG

konva

Version:

HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.

1,018 lines (1,017 loc) 40.2 kB
import { Util } from "./Util.js"; import { Factory } from "./Factory.js"; import { Container } from "./Container.js"; import { Konva } from "./Global.js"; import { SceneCanvas, HitCanvas } from "./Canvas.js"; import { DD } from "./DragAndDrop.js"; import { _registerNode } from "./Global.js"; import * as PointerEvents from "./PointerEvents.js"; // CONSTANTS const STAGE = 'Stage', STRING = 'string', PX = 'px', MOUSEOUT = 'mouseout', MOUSELEAVE = 'mouseleave', MOUSEOVER = 'mouseover', MOUSEENTER = 'mouseenter', MOUSEMOVE = 'mousemove', MOUSEDOWN = 'mousedown', MOUSEUP = 'mouseup', POINTERMOVE = 'pointermove', POINTERDOWN = 'pointerdown', POINTERUP = 'pointerup', POINTERCANCEL = 'pointercancel', LOSTPOINTERCAPTURE = 'lostpointercapture', POINTEROUT = 'pointerout', POINTERLEAVE = 'pointerleave', POINTEROVER = 'pointerover', POINTERENTER = 'pointerenter', CONTEXTMENU = 'contextmenu', TOUCHSTART = 'touchstart', TOUCHEND = 'touchend', TOUCHMOVE = 'touchmove', TOUCHCANCEL = 'touchcancel', WHEEL = 'wheel', MAX_LAYERS_NUMBER = 5, EVENTS = [ [MOUSEENTER, '_pointerenter'], [MOUSEDOWN, '_pointerdown'], [MOUSEMOVE, '_pointermove'], [MOUSEUP, '_pointerup'], [MOUSELEAVE, '_pointerleave'], [TOUCHSTART, '_pointerdown'], [TOUCHMOVE, '_pointermove'], [TOUCHEND, '_pointerup'], [TOUCHCANCEL, '_pointercancel'], [MOUSEOVER, '_pointerover'], [WHEEL, '_wheel'], [CONTEXTMENU, '_contextmenu'], [POINTERDOWN, '_pointerdown'], [POINTERMOVE, '_pointermove'], [POINTERUP, '_pointerup'], [POINTERCANCEL, '_pointercancel'], [POINTERLEAVE, '_pointerleave'], [LOSTPOINTERCAPTURE, '_lostpointercapture'], ]; const EVENTS_MAP = { mouse: { [POINTEROUT]: MOUSEOUT, [POINTERLEAVE]: MOUSELEAVE, [POINTEROVER]: MOUSEOVER, [POINTERENTER]: MOUSEENTER, [POINTERMOVE]: MOUSEMOVE, [POINTERDOWN]: MOUSEDOWN, [POINTERUP]: MOUSEUP, [POINTERCANCEL]: 'mousecancel', pointerclick: 'click', pointerdblclick: 'dblclick', }, touch: { [POINTEROUT]: 'touchout', [POINTERLEAVE]: 'touchleave', [POINTEROVER]: 'touchover', [POINTERENTER]: 'touchenter', [POINTERMOVE]: TOUCHMOVE, [POINTERDOWN]: TOUCHSTART, [POINTERUP]: TOUCHEND, [POINTERCANCEL]: TOUCHCANCEL, pointerclick: 'tap', pointerdblclick: 'dbltap', }, pointer: { [POINTEROUT]: POINTEROUT, [POINTERLEAVE]: POINTERLEAVE, [POINTEROVER]: POINTEROVER, [POINTERENTER]: POINTERENTER, [POINTERMOVE]: POINTERMOVE, [POINTERDOWN]: POINTERDOWN, [POINTERUP]: POINTERUP, [POINTERCANCEL]: POINTERCANCEL, pointerclick: 'pointerclick', pointerdblclick: 'pointerdblclick', }, }; const getEventsMap = (eventType) => { const type = Util._getEventType(eventType); if (type === 'pointer') { return Konva.pointerEventsEnabled && EVENTS_MAP.pointer; } if (type === 'touch') { return EVENTS_MAP.touch; } if (type === 'mouse') { return EVENTS_MAP.mouse; } }; function checkNoClip(attrs = {}) { if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) { Util.warn('Stage does not support clipping. Please use clip for Layers or Groups.'); } return attrs; } 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);`; export const stages = []; // A stage may be rendered in another window than the one Konva was imported // into: an iframe, or a window opened with `window.open`. Such a window sends // its own events and has its own visibility state, so Konva listens to every // window it renders a stage in const listeningWindows = new WeakSet(); const listenToWindow = (win) => { if (!win || listeningWindows.has(win)) { return; } listeningWindows.add(win); DD._listenToWindow(win); // chrome is clearing canvas in inactive browser window, causing layer content to be erased // so let's redraw layers as soon as window becomes active win.document.addEventListener('visibilitychange', () => { stages.forEach((stage) => { stage.batchDraw(); }); }); }; /** * 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 { constructor(config) { super(checkNoClip(config)); this._pointerPositions = []; this._changedPointerPositions = []; this._pointerStates = new Map(); this._lastClicks = {}; 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(); } _validateAdd(child) { const isLayer = child.getType() === 'Layer'; const isFastLayer = child.getType() === 'FastLayer'; const valid = isLayer || isFastLayer; if (!valid) { Util.throw('You may only add layers to the stage.'); } } _checkVisibility() { if (!this.content) { return; } const style = this.visible() ? '' : 'none'; this.content.style.display = style; } /** * The window the stage is rendered in. It is not always the window Konva was * imported into: the container may belong to an iframe, or to a window * opened with `window.open`. */ _getOwnerWindow() { var _a; const element = this.content || this.container(); // a document with no browsing context, such as one of `DOMParser`, has no // window of its own. The window Konva was imported into is used then return (((_a = element === null || element === void 0 ? void 0 : element.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView) || (Konva.isBrowser ? window : null)); } /** * set container dom element which contains the stage wrapper div element * @method * @name Konva.Stage#setContainer * @param {DomElement} container can pass in a dom element or id string */ setContainer(container) { if (typeof container === STRING) { let id; if (container.charAt(0) === '.') { const className = container.slice(1); container = document.getElementsByClassName(className)[0]; } else { 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); // the new container may be in another window, with its own events listenToWindow(this._getOwnerWindow()); } return this; } shouldDrawHit() { return true; } /** * clear all layers * @method * @name Konva.Stage#clear */ clear() { const layers = this.children, len = layers.length; for (let n = 0; n < len; n++) { layers[n].clear(); } return this; } clone(obj) { if (!obj) { obj = {}; } obj.container = typeof document !== 'undefined' && document.createElement('div'); return Container.prototype.clone.call(this, obj); } destroy() { var _a; super.destroy(); this._pointerStates.clear(); this._lastClicks = {}; const content = this.content; (_a = content === null || content === void 0 ? void 0 : content.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(content); const index = stages.indexOf(this); if (index > -1) { stages.splice(index, 1); } Util.releaseCanvas(this.bufferCanvas._canvas, this.bufferHitCanvas._canvas); 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 canvas * @method * @name Konva.Stage#getPointerPosition * @returns {Vector2d|null} */ getPointerPosition() { const pos = this._pointerPositions[0] || this._changedPointerPositions[0]; if (!pos) { Util.warn(NO_POINTERS_MESSAGE); return null; } return { x: pos.x, y: pos.y, }; } _getPointerById(id) { return this._pointerPositions.find((p) => p.id === id); } getPointersPositions() { return this._pointerPositions; } 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(); const canvas = new SceneCanvas({ width: config.width, height: config.height, pixelRatio: config.pixelRatio || 1, }); const _context = canvas.getContext()._context; const layers = this.children; if (config.x || config.y) { _context.translate(-1 * config.x, -1 * config.y); } // every layer renders on its own canvas, like on screen, so a // globalCompositeOperation inside a layer does not reach the layers below layers.forEach(function (layer) { if (!layer.isVisible()) { return; } const layerCanvas = layer._toKonvaCanvas(config); _context.drawImage(layerCanvas._canvas, config.x, config.y, layerCanvas.getWidth() / layerCanvas.getPixelRatio(), layerCanvas.getHeight() / layerCanvas.getPixelRatio()); Util.releaseCanvas(layerCanvas._canvas); }); return canvas; } /** * get visible intersection shape. This is the preferred * method for determining if a point intersects a shape or not. * It reads the hit canvas, so it finds what pointer events would: nodes with * listening set to false or invisible nodes are not detected, a shape with opacity 0 * is, and `hitStrokeWidth` counts. The position is relative to the top left corner of the * stage container, like `stage.getPointerPosition()`, without the stage transform * @method * @name Konva.Stage#getIntersection * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Konva.Node} * @example * var shape = stage.getIntersection({x: 50, y: 50}); */ getIntersection(pos) { if (!pos) { return null; } const layers = this.children, len = layers.length, end = len - 1; for (let n = end; n >= 0; n--) { const shape = layers[n].getIntersection(pos); if (shape) { return shape; } } return null; } _resizeDOM() { const width = this.width(); const height = this.height(); if (this.content) { // set content dimensions this.content.style.width = width + PX; this.content.style.height = height + PX; } // set layer dimensions this.children.forEach((layer) => { layer._setSize({ width, height }); layer.draw(); }); } // "perfect drawing" uses bufferCanvas, shape.intersects() uses // bufferHitCanvas; both are lazy - created at 0x0 and sized to the stage // on first use _syncBufferSize(canvas) { canvas.setSizeIfChanged(this.width(), this.height()); return canvas; } add(layer, ...rest) { if (arguments.length > 1) { for (let i = 0; i < arguments.length; i++) { this.add(arguments[i]); } return this; } super.add(layer); const length = this.children.length; if (length > MAX_LAYERS_NUMBER) { Util.warn('The stage has ' + length + ' 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() }); // draw layer and append canvas to container layer.draw(); if (Konva.isBrowser) { this.content.appendChild(layer.canvas._canvas); } // chainable return this; } getParent() { return null; } getLayer() { return null; } hasPointerCapture(pointerId) { return PointerEvents.hasPointerCapture(pointerId, this); } setPointerCapture(pointerId) { PointerEvents.setPointerCapture(pointerId, this); } releaseCapture(pointerId) { PointerEvents.releaseCapture(pointerId, this); } /** * returns an array of layers * @method * @name Konva.Stage#getLayers */ getLayers() { return this.getChildren(); } _bindContentEvents() { if (!Konva.isBrowser) { return; } EVENTS.forEach(([event, methodName]) => { this.content.addEventListener(event, (evt) => { this._batchEvents(() => this[methodName](evt)); }, { passive: false }); }); } _batchEvents(callback) { Util._batchEvents(this.eventBatchFunc(), callback); } _pointerenter(evt) { this.setPointersPositions(evt); const events = getEventsMap(evt.type); if (events) { this._fire(events.pointerenter, { evt: evt, target: this, currentTarget: this, }); } } _pointerover(evt) { this.setPointersPositions(evt); const events = getEventsMap(evt.type); if (events) { this._fire(events.pointerover, { evt: evt, target: this, currentTarget: this, }); } } _pointerEventsEnabled() { if (Konva.hitOnDragEnabled) return true; for (const { node, dragStatus } of DD._dragElements.values()) { if (dragStatus === 'dragging' && node.getStage() === this) return false; } return !this.getLayers().some((layer) => { var _a; return (_a = Konva['Transformer']) === null || _a === void 0 ? void 0 : _a._isLayerTransforming(layer); }); } _cancelClick(pointerId, eventType) { const types = eventType ? [eventType] : ['mouse', 'touch', 'pointer']; for (const type of types) { const state = this._pointerStates.get(`${type}:${pointerId}`); if (!state) continue; state.clickStartShape = undefined; if (state.relatedPointer) state.relatedPointer.clickStartShape = undefined; } } _pointerleave(evt) { var _a, _b; const events = getEventsMap(evt.type); const eventType = Util._getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); const pointerId = (_a = this._changedPointerPositions[0]) === null || _a === void 0 ? void 0 : _a.id; const key = `${eventType}:${pointerId}`; const state = this._pointerStates.get(key); const targetShape = ((_b = state === null || state === void 0 ? void 0 : state.targetShape) === null || _b === void 0 ? void 0 : _b.getStage()) === this ? state.targetShape : undefined; // A released touch will not return to deliver a deferred hover exit. const eventsEnabled = this._pointerEventsEnabled() || (evt.pointerType === 'touch' && !(state === null || state === void 0 ? void 0 : state.downPosition)); if (eventsEnabled) { if ((state === null || state === void 0 ? void 0 : state.clickStartShape) !== undefined) { state.targetShape = undefined; } else { this._pointerStates.delete(key); } } if (targetShape && eventsEnabled) { targetShape._fireAndBubble(events.pointerout, { evt, pointerId }); targetShape._fireAndBubble(events.pointerleave, { evt, pointerId }); this._fire(events.pointerleave, { evt: evt, target: this, currentTarget: this, }); } else if (eventsEnabled) { this._fire(events.pointerleave, { evt: evt, target: this, currentTarget: this, }); this._fire(events.pointerout, { evt: evt, target: this, currentTarget: this, }); } this.pointerPos = null; this._pointerPositions = []; this._changedPointerPositions = []; this._pointerEventType = undefined; } _pointerdown(evt) { const events = getEventsMap(evt.type); const eventType = Util._getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); let triggeredOnShape = false; this._changedPointerPositions.forEach((pos) => { var _a; const shape = this.getIntersection(pos); const key = `${eventType}:${pos.id}`; const state = { targetShape: (_a = this._pointerStates.get(key)) === null || _a === void 0 ? void 0 : _a.targetShape, clickStartShape: shape && shape.isListening() ? shape : null, lastClick: this._lastClicks[eventType], downPosition: pos, pointerType: evt.pointerType, }; // A native pointerdown precedes its compatibility mouse/touch event. // Keep that association for the press: IDs can differ, and mouse // coordinates can be rounded by the browser. this._pointerStates.delete(key); if (eventType !== 'pointer') { for (const [pointerKey, pointer] of [ ...this._pointerStates, ].reverse()) { if (!pointerKey.startsWith('pointer:') || !pointer.downPosition || (eventType === 'touch' && pointer.relatedPointer)) continue; const matches = eventType === 'mouse' ? pointer.pointerType !== 'touch' : pointer.pointerType === 'touch' && pointer.downPosition.x === pos.x && pointer.downPosition.y === pos.y; if (matches) { state.relatedPointer = pointer; pointer.relatedPointer = state; if (pointer.clickStartShape === undefined) state.clickStartShape = undefined; break; } } } this._pointerStates.set(key, state); for (const elem of DD._dragElements.values()) { if (elem.dragStatus === 'dragging' && (elem.pointerId === undefined || (elem.pointerEventType === eventType && elem.pointerId === pos.id) || (elem.pointerEventType === 'mouse' && eventType === 'pointer' && state.pointerType !== 'touch')) && elem.node.getStage() === this) { // Drag movement uses the compatibility mouse/touch pointer. if (elem.pointerId === undefined && eventType !== 'pointer') { elem.pointerId = pos.id; elem.pointerEventType = eventType; } this._cancelClick(pos.id, eventType); } } // no shape detected? do nothing if (!shape || !shape.isListening()) { return; } if (Konva.capturePointerEventsEnabled) { shape.setPointerCapture(pos.id); } shape._fireAndBubble(events.pointerdown, { evt: evt, pointerId: pos.id, }); triggeredOnShape = true; // only call preventDefault if the shape is listening for events const isTouch = evt.type.indexOf('touch') >= 0; if (shape.preventDefault() && evt.cancelable && isTouch) { evt.preventDefault(); } }); // trigger down on stage if not already if (!triggeredOnShape) { this._fire(events.pointerdown, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); } } _pointermove(evt) { const events = getEventsMap(evt.type); const eventType = Util._getEventType(evt.type); if (!events) { return; } // prevent default only for touch-based interactions to avoid blocking // native mouse wheel scrolling during drag on desktop const isTouchPointer = evt.type.indexOf('touch') >= 0 || evt.pointerType === 'touch'; if (evt.cancelable && isTouchPointer) { for (const { node, dragStatus } of DD._dragElements.values()) { if (dragStatus === 'dragging' && node.getStage() === this && node.preventDefault()) { evt.preventDefault(); break; } } } this.setPointersPositions(evt); const eventsEnabled = this._pointerEventsEnabled(); if (!eventsEnabled) { return; } const processedShapesIds = {}; let triggeredOnShape = false; this._changedPointerPositions.forEach((pos) => { var _a; const key = `${eventType}:${pos.id}`; let state = this._pointerStates.get(key); if (!state) { state = {}; this._pointerStates.set(key, state); } const shape = (PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos)); const targetShape = state.targetShape === shape || ((_a = state.targetShape) === null || _a === void 0 ? void 0 : _a.getStage()) === this ? state.targetShape : undefined; const pointerId = pos.id; const event = { evt: evt, pointerId }; const differentTarget = targetShape !== shape; state.targetShape = shape && shape.isListening() ? shape : undefined; if (differentTarget && targetShape) { targetShape._fireAndBubble(events.pointerout, { ...event }, shape); targetShape._fireAndBubble(events.pointerleave, { ...event }, shape); } if (shape && shape.isListening()) { triggeredOnShape = true; if (differentTarget) { shape._fireAndBubble(events.pointerover, { ...event }, targetShape); shape._fireAndBubble(events.pointerenter, { ...event }, targetShape); } if (!processedShapesIds[shape._id]) { processedShapesIds[shape._id] = true; shape._fireAndBubble(events.pointermove, { ...event }); } } else { if (targetShape) { this._fire(events.pointerover, { evt: evt, target: this, currentTarget: this, pointerId, }); } } }); if (!triggeredOnShape) { this._fire(events.pointermove, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); } } _pointerup(evt) { const events = getEventsMap(evt.type); const eventType = Util._getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); const processedShapesIds = {}; let skipPointerUpTrigger = false; this._changedPointerPositions.forEach((pos) => { var _a; const key = `${eventType}:${pos.id}`; const state = this._pointerStates.get(key); const listenClick = (state === null || state === void 0 ? void 0 : state.clickStartShape) !== undefined; const clickStartShape = state === null || state === void 0 ? void 0 : state.clickStartShape; if ((state === null || state === void 0 ? void 0 : state.targetShape) && eventType !== 'touch') { this._pointerStates.set(key, { targetShape: state.targetShape }); } else { this._pointerStates.delete(key); } const shape = (PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos)); const alreadyReleased = shape && processedShapesIds[shape._id]; if (shape) { shape.releaseCapture(pos.id); processedShapesIds[shape._id] = true; } const pointerId = pos.id; const event = { evt: evt, pointerId }; const clickTarget = shape && shape.isListening() ? shape : null; const lastClick = this._lastClicks[eventType]; const canClick = listenClick && (!clickTarget || clickStartShape === clickTarget); const fireDblClick = canClick && lastClick && (state === null || state === void 0 ? void 0 : state.lastClick) === lastClick && lastClick.shape === clickTarget && Date.now() - lastClick.time < Konva.dblClickWindow; if (canClick) { this._lastClicks[eventType] = fireDblClick ? undefined : { shape: clickTarget, time: Date.now() }; } if (shape && shape.isListening()) { skipPointerUpTrigger = true; if (!alreadyReleased) { shape._fireAndBubble(events.pointerup, { ...event }); } // detect if click or double click occurred if (listenClick && clickStartShape === shape) { shape._fireAndBubble(events.pointerclick, { ...event }); if (fireDblClick) { shape._fireAndBubble(events.pointerdblclick, { ...event }); } } } else { if (!skipPointerUpTrigger) { this._fire(events.pointerup, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); skipPointerUpTrigger = true; } if (listenClick) { this._fire(events.pointerclick, { evt: evt, target: this, currentTarget: this, pointerId, }); } if (fireDblClick) { this._fire(events.pointerdblclick, { evt: evt, target: this, currentTarget: this, pointerId, }); } } if (eventType === 'touch' && ((_a = state === null || state === void 0 ? void 0 : state.targetShape) === null || _a === void 0 ? void 0 : _a.getStage()) === this) { state.targetShape._fireAndBubble(events.pointerout, { ...event }); state.targetShape._fireAndBubble(events.pointerleave, { ...event }); } }); if (!skipPointerUpTrigger) { this._fire(events.pointerup, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); } // always call preventDefault for desktop events because some browsers // try to drag and drop the canvas element // do not call this function on mobile because it prevent "click" event on all parent containers // but apps may listen to it. if (evt.cancelable && eventType !== 'touch' && eventType !== 'pointer') { evt.preventDefault(); } } _contextmenu(evt) { this.setPointersPositions(evt); const shape = this.getIntersection(this.getPointerPosition()); if (shape && shape.isListening()) { shape._fireAndBubble(CONTEXTMENU, { evt: evt }); } else { this._fire(CONTEXTMENU, { evt: evt, target: this, currentTarget: this, }); } } _wheel(evt) { this.setPointersPositions(evt); const shape = this.getIntersection(this.getPointerPosition()); if (shape && shape.isListening()) { shape._fireAndBubble(WHEEL, { evt: evt }); } else { this._fire(WHEEL, { evt: evt, target: this, currentTarget: this, }); } } _pointercancel(evt) { this.setPointersPositions(evt); const events = getEventsMap(evt.type); this._changedPointerPositions.forEach((pos) => { var _a; const eventType = Util._getEventType(evt.type); const key = `${eventType}:${pos.id}`; this._cancelClick(pos.id, eventType); const targetShape = (_a = this._pointerStates.get(key)) === null || _a === void 0 ? void 0 : _a.targetShape; if (targetShape && eventType !== 'touch') { this._pointerStates.set(key, { targetShape }); } else { this._pointerStates.delete(key); } const shape = PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos); const event = { evt, pointerId: pos.id }; if (shape) { if (events) shape._fireAndBubble(events.pointercancel, { ...event }); // Keep the historical pointerup notification for cancellation listeners. shape._fireAndBubble(POINTERUP, { ...event }); } else if (events) { this._fire(events.pointercancel, { ...event, target: this, currentTarget: this, }); } PointerEvents.releaseCapture(pos.id); if (events && eventType === 'touch' && (targetShape === null || targetShape === void 0 ? void 0 : targetShape.getStage()) === this) { targetShape._fireAndBubble(events.pointerout, { ...event }); targetShape._fireAndBubble(events.pointerleave, { ...event }); } }); } _lostpointercapture(evt) { const captured = PointerEvents.getCapturedShape(evt.pointerId, this); if (captured) { 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} evt Event object * @example * * window.addEventListener('mousemove', (e) => { * stage.setPointersPositions(e); * }); */ setPointersPositions(evt) { const contentPosition = this._getContentPosition(); let x = null, y = null; evt = evt ? evt : window.event; // touch events if (evt.touches !== undefined) { this._pointerEventType = 'touch'; // touchlist has not support for map method // so we have to iterate this._pointerPositions = []; this._changedPointerPositions = []; Array.prototype.forEach.call(evt.touches, (touch) => { this._pointerPositions.push({ id: touch.identifier, x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, }); }); Array.prototype.forEach.call(evt.changedTouches || evt.touches, (touch) => { this._changedPointerPositions.push({ id: touch.identifier, x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, }); }); } else { // mouse events this._pointerEventType = evt.pointerId === undefined ? 'mouse' : 'pointer'; x = (evt.clientX - contentPosition.left) / contentPosition.scaleX; y = (evt.clientY - contentPosition.top) / contentPosition.scaleY; this.pointerPos = { x: x, y: y, }; this._pointerPositions = [{ x, y, id: Util._getFirstPointerId(evt) }]; this._changedPointerPositions = [ { x, y, id: Util._getFirstPointerId(evt) }, ]; } } _setPointerPosition(evt) { Util.warn('Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'); this.setPointersPositions(evt); } _getContentPosition() { if (!this.content || !this.content.getBoundingClientRect) { return { top: 0, left: 0, scaleX: 1, scaleY: 1, }; } const rect = this.content.getBoundingClientRect(); return { top: rect.top, left: rect.left, // 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, }; } _buildDOM() { // lazy buffer canvases, see _syncBufferSize // https://github.com/konvajs/konva/issues/2009 this.bufferCanvas = new SceneCanvas({ width: 0, height: 0, }); this.bufferHitCanvas = new HitCanvas({ pixelRatio: 1, width: 0, height: 0, }); if (!Konva.isBrowser) { return; } const container = this.container(); if (!container) { throw 'Stage has no container. A container is required.'; } // clear content inside container container.innerHTML = ''; // content // the container may belong to another document - an iframe, or a popout // window - so the content is created in the document of the container this.content = container.ownerDocument.createElement('div'); this.content.style.position = 'relative'; this.content.style.userSelect = 'none'; this.content.className = 'konvajs-content'; this.content.setAttribute('role', 'presentation'); container.appendChild(this.content); listenToWindow(this._getOwnerWindow()); 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; } /** * batch draw * @method * @name Konva.Stage#batchDraw * @return {Konva.Stage} this */ batchDraw() { var _a; // the config applied by the Node constructor already requests a draw, // before the children array exists (_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (layer) { layer.batchDraw(); }); return this; } } Stage.prototype.nodeType = STAGE; _registerNode(Stage); /** * get/set container DOM element. The container may belong to another window * than the one Konva was imported into: an iframe, or a window opened with * `window.open`. The stage then uses the events and the frames of that window. * @method * @name Konva.Stage#container * @returns {DomElement} container * @example * // get container * var container = stage.container(); * // set container * var container = document.createElement('div'); * body.appendChild(container); * stage.container(container); * * // a stage in a popout window * var popout = window.open('', '', 'width=600,height=400'); * stage.container(popout.document.body); */ Factory.addGetterSetter(Stage, 'container'); /** * Get/set the callback used by framework integrations to batch native input. * The callback must invoke its argument exactly once, synchronously. Existing * node events keep their order and cancellation behavior. Direct node.fire() * and programmatic mutations do not create a batch. * Missing calls warn; duplicate or late calls warn and are ignored. Exceptions * propagate and skipped input is not replayed, so an invalid callback can * prevent input processing and drag cleanup. * Each native Konva listener creates its own scope; a DOM event may invoke * several listeners. Batching does not reorder or merge those listeners. * @method * @name Konva.Stage#eventBatchFunc * @example * stage.eventBatchFunc((run) => frameworkBatch(run)); */ Factory.addGetterSetter(Stage, 'eventBatchFunc');