diff --git a/core/block_dragger.ts b/core/block_dragger.ts index 0469a325a..c977f5c53 100644 --- a/core/block_dragger.ts +++ b/core/block_dragger.ts @@ -176,7 +176,7 @@ export class BlockDragger implements IBlockDragger { * @param currentDragDeltaXY How far the pointer has moved from the position * at the start of the drag, in pixel units. */ - drag(e: Event, currentDragDeltaXY: Coordinate) { + drag(e: PointerEvent, currentDragDeltaXY: Coordinate) { const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const newLoc = Coordinate.sum(this.startXY_, delta); this.draggingBlock_.moveDuringDrag(newLoc); @@ -205,11 +205,11 @@ export class BlockDragger implements IBlockDragger { /** * Finish a block drag and put the block back on the workspace. * - * @param e The mouseup/touchend event. + * @param e The pointerup event. * @param currentDragDeltaXY How far the pointer has moved from the position * at the start of the drag, in pixel units. */ - endDrag(e: Event, currentDragDeltaXY: Coordinate) { + endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) { // Make sure internal state is fresh. this.drag(e, currentDragDeltaXY); this.dragIconData_ = []; diff --git a/core/block_svg.ts b/core/block_svg.ts index 615e568c4..02e2f0d0a 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -227,7 +227,8 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, this.pathObject.updateMovable(this.isMovable()); const svg = this.getSvgRoot(); if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) { - browserEvents.conditionalBind(svg, 'mousedown', this, this.onMouseDown_); + browserEvents.conditionalBind( + svg, 'pointerdown', this, this.onMouseDown_); } this.eventsInit_ = true; @@ -673,11 +674,11 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, } /** - * Handle a mouse-down on an SVG block. + * Handle a pointerdown on an SVG block. * - * @param e Mouse down event or touch start event. + * @param e Pointer down event. */ - private onMouseDown_(e: Event) { + private onMouseDown_(e: PointerEvent) { const gesture = this.workspace.getGesture(e); if (gesture) { gesture.handleBlockStart(e, this); diff --git a/core/blockly.ts b/core/blockly.ts index 42c75ee79..0537827a9 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -145,7 +145,6 @@ import {Toolbox} from './toolbox/toolbox.js'; import {ToolboxItem} from './toolbox/toolbox_item.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; -import {TouchGesture} from './touch_gesture.js'; import {Trashcan} from './trashcan.js'; import * as utils from './utils.js'; import * as colour from './utils/colour.js'; @@ -485,9 +484,7 @@ export function unbindEvent_(bindData: browserEvents.Data): Function { * @param opt_noCaptureIdentifier True if triggering on this event should not * block execution of other event handlers on this touch or other * simultaneous touches. False by default. - * @param opt_noPreventDefault True if triggering on this event should prevent - * the default handler. False by default. If opt_noPreventDefault is - * provided, opt_noCaptureIdentifier must also be provided. + * @param _opt_noPreventDefault No-op, deprecated and will be removed in v10. * @returns Opaque data that can be passed to unbindEvent_. * @deprecated Use **Blockly.browserEvents.conditionalBind** instead. * @see browserEvents.conditionalBind @@ -496,13 +493,12 @@ export function unbindEvent_(bindData: browserEvents.Data): Function { export function bindEventWithChecks_( node: EventTarget, name: string, thisObject: Object|null, func: Function, opt_noCaptureIdentifier?: boolean, - opt_noPreventDefault?: boolean): browserEvents.Data { + _opt_noPreventDefault?: boolean): browserEvents.Data { deprecation.warn( 'Blockly.bindEventWithChecks_', 'December 2021', 'December 2022', 'Blockly.browserEvents.conditionalBind'); return browserEvents.conditionalBind( - node, name, thisObject, func, opt_noCaptureIdentifier, - opt_noPreventDefault); + node, name, thisObject, func, opt_noCaptureIdentifier); } // Aliases to allow external code to access these values for legacy reasons. @@ -671,6 +667,7 @@ export {FlyoutMetricsManager}; export {CodeGenerator}; export {CodeGenerator as Generator}; // Deprecated name, October 2022. export {Gesture}; +export {Gesture as TouchGesture}; // Remove in v10. export {Grid}; export {HorizontalFlyout}; export {IASTNodeLocation}; @@ -723,7 +720,6 @@ export {Toolbox}; export {ToolboxCategory}; export {ToolboxItem}; export {ToolboxSeparator}; -export {TouchGesture}; export {Trashcan}; export {VariableMap}; export {VariableModel}; diff --git a/core/browser_events.ts b/core/browser_events.ts index efed4a34d..a36c7e537 100644 --- a/core/browser_events.ts +++ b/core/browser_events.ts @@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.browserEvents'); import * as Touch from './touch.js'; +import * as deprecation from './utils/deprecation.js'; import * as userAgent from './utils/useragent.js'; @@ -51,42 +52,36 @@ const PAGE_MODE_MULTIPLIER = 125; * @param opt_noCaptureIdentifier True if triggering on this event should not * block execution of other event handlers on this touch or other * simultaneous touches. False by default. - * @param opt_noPreventDefault True if triggering on this event should prevent - * the default handler. False by default. If opt_noPreventDefault is - * provided, opt_noCaptureIdentifier must also be provided. + * @param opt_noPreventDefault No-op, deprecated and will be removed in v10. * @returns Opaque data that can be passed to unbindEvent_. * @alias Blockly.browserEvents.conditionalBind */ export function conditionalBind( node: EventTarget, name: string, thisObject: Object|null, func: Function, opt_noCaptureIdentifier?: boolean, opt_noPreventDefault?: boolean): Data { - let handled = false; + if (opt_noPreventDefault !== undefined) { + deprecation.warn( + 'The opt_noPreventDefault argument of conditionalBind', 'version 9', + 'version 10'); + } /** * * @param e */ function wrapFunc(e: Event) { const captureIdentifier = !opt_noCaptureIdentifier; - // Handle each touch point separately. If the event was a mouse event, this - // will hand back an array with one element, which we're fine handling. - const events = Touch.splitEventByTouches(e); - for (let i = 0; i < events.length; i++) { - const event = events[i]; - if (captureIdentifier && !Touch.shouldHandleEvent(event)) { - continue; - } - Touch.setClientFromTouch(event); + + if (!(captureIdentifier && !Touch.shouldHandleEvent(e))) { if (thisObject) { - func.call(thisObject, event); + func.call(thisObject, e); } else { - func(event); + func(e); } - handled = true; } } const bindData: Data = []; - if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) { + if (name in Touch.TOUCH_MAP) { for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { const type = Touch.TOUCH_MAP[name][i]; node.addEventListener(type, wrapFunc, false); @@ -95,24 +90,6 @@ export function conditionalBind( } else { node.addEventListener(name, wrapFunc, false); bindData.push([node, name, wrapFunc]); - - // Add equivalent touch event. - if (name in Touch.TOUCH_MAP) { - const touchWrapFunc = (e: Event) => { - wrapFunc(e); - // Calling preventDefault stops the browser from scrolling/zooming the - // page. - const preventDef = !opt_noPreventDefault; - if (handled && preventDef) { - e.preventDefault(); - } - }; - for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { - const type = Touch.TOUCH_MAP[name][i]; - node.addEventListener(type, touchWrapFunc, false); - bindData.push([node, type, touchWrapFunc]); - } - } } return bindData; } @@ -146,7 +123,7 @@ export function bind( } const bindData: Data = []; - if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) { + if (name in Touch.TOUCH_MAP) { for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { const type = Touch.TOUCH_MAP[name][i]; node.addEventListener(type, wrapFunc, false); @@ -155,32 +132,6 @@ export function bind( } else { node.addEventListener(name, wrapFunc, false); bindData.push([node, name, wrapFunc]); - - // Add equivalent touch event. - if (name in Touch.TOUCH_MAP) { - const touchWrapFunc = (e: Event) => { - // Punt on multitouch events. - if (e instanceof TouchEvent && e.changedTouches && - e.changedTouches.length === 1) { - // Map the touch event's properties to the event. - const touchPoint = e.changedTouches[0]; - // TODO (6311): We are trying to make a touch event look like a mouse - // event, which is not allowed, because it requires adding more - // properties to the event. How do we want to deal with this? - (e as AnyDuringMigration).clientX = touchPoint.clientX; - (e as AnyDuringMigration).clientY = touchPoint.clientY; - } - wrapFunc(e); - - // Stop the browser from scrolling/zooming the page. - e.preventDefault(); - }; - for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) { - const type = Touch.TOUCH_MAP[name][i]; - node.addEventListener(type, touchWrapFunc, false); - bindData.push([node, type, touchWrapFunc]); - } - } } return bindData; } diff --git a/core/bubble.ts b/core/bubble.ts index 000ae773e..054043c2e 100644 --- a/core/bubble.ts +++ b/core/bubble.ts @@ -249,10 +249,10 @@ export class Bubble implements IBubble { if (!this.workspace_.options.readOnly) { this.onMouseDownBubbleWrapper = browserEvents.conditionalBind( - this.bubbleBack, 'mousedown', this, this.bubbleMouseDown); + this.bubbleBack, 'pointerdown', this, this.bubbleMouseDown); if (this.resizeGroup) { this.onMouseDownResizeWrapper = browserEvents.conditionalBind( - this.resizeGroup, 'mousedown', this, this.resizeMouseDown); + this.resizeGroup, 'pointerdown', this, this.resizeMouseDown); } } this.bubbleGroup.appendChild(content); @@ -278,11 +278,11 @@ export class Bubble implements IBubble { } /** - * Handle a mouse-down on bubble's border. + * Handle a pointerdown on bubble's border. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private bubbleMouseDown(e: Event) { + private bubbleMouseDown(e: PointerEvent) { const gesture = this.workspace_.getGesture(e); if (gesture) { gesture.handleBubbleStart(e, this); @@ -318,11 +318,11 @@ export class Bubble implements IBubble { // NOP if bubble is not deletable. /** - * Handle a mouse-down on bubble's resize corner. + * Handle a pointerdown on bubble's resize corner. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private resizeMouseDown(e: MouseEvent) { + private resizeMouseDown(e: PointerEvent) { this.promote(); Bubble.unbindDragEvents(); if (browserEvents.isRightButton(e)) { @@ -337,20 +337,20 @@ export class Bubble implements IBubble { this.workspace_.RTL ? -this.width : this.width, this.height)); Bubble.onMouseUpWrapper = browserEvents.conditionalBind( - document, 'mouseup', this, Bubble.bubbleMouseUp); + document, 'pointerup', this, Bubble.bubbleMouseUp); Bubble.onMouseMoveWrapper = browserEvents.conditionalBind( - document, 'mousemove', this, this.resizeMouseMove); + document, 'pointermove', this, this.resizeMouseMove); this.workspace_.hideChaff(); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); } /** - * Resize this bubble to follow the mouse. + * Resize this bubble to follow the pointer. * - * @param e Mouse move event. + * @param e Pointer move event. */ - private resizeMouseMove(e: MouseEvent) { + private resizeMouseMove(e: PointerEvent) { this.autoLayout = false; const newXY = this.workspace_.moveDrag(e); this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y); @@ -847,11 +847,11 @@ export class Bubble implements IBubble { } /** - * Handle a mouse-up event while dragging a bubble's border or resize handle. + * Handle a pointerup event while dragging a bubble's border or resize handle. * - * @param _e Mouse up event. + * @param _e Pointer up event. */ - private static bubbleMouseUp(_e: MouseEvent) { + private static bubbleMouseUp(_e: PointerEvent) { Touch.clearTouchIdentifier(); Bubble.unbindDragEvents(); } diff --git a/core/bubble_dragger.ts b/core/bubble_dragger.ts index 1ac916fc4..edd67655b 100644 --- a/core/bubble_dragger.ts +++ b/core/bubble_dragger.ts @@ -89,7 +89,7 @@ export class BubbleDragger { * at the start of the drag, in pixel units. * @internal */ - dragBubble(e: Event, currentDragDeltaXY: Coordinate) { + dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) { const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const newLoc = Coordinate.sum(this.startXY_, delta); this.bubble.moveDuringDrag(this.dragSurface_, newLoc); @@ -141,12 +141,12 @@ export class BubbleDragger { /** * Finish a bubble drag and put the bubble back on the workspace. * - * @param e The mouseup/touchend event. + * @param e The pointerup event. * @param currentDragDeltaXY How far the pointer has moved from the position * at the start of the drag, in pixel units. * @internal */ - endBubbleDrag(e: Event, currentDragDeltaXY: Coordinate) { + endBubbleDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) { // Make sure internal state is fresh. this.dragBubble(e, currentDragDeltaXY); diff --git a/core/comment.ts b/core/comment.ts index 6b383a8ac..d716aa2f0 100644 --- a/core/comment.ts +++ b/core/comment.ts @@ -149,11 +149,8 @@ export class Comment extends Icon { body.appendChild(textarea); this.foreignObject!.appendChild(body); - // Ideally this would be hooked to the focus event for the comment. - // However doing so in Firefox swallows the cursor for unknown reasons. - // So this is hooked to mouseup instead. No big deal. this.onMouseUpWrapper = browserEvents.conditionalBind( - textarea, 'mouseup', this, this.startEdit, true, true); + textarea, 'focus', this, this.startEdit, true); // Don't zoom with mousewheel. this.onWheelWrapper = browserEvents.conditionalBind( textarea, 'wheel', this, function(e: Event) { @@ -315,7 +312,7 @@ export class Comment extends Icon { * * @param _e Mouse up event. */ - private startEdit(_e: Event) { + private startEdit(_e: PointerEvent) { if (this.bubble_?.promote()) { // Since the act of moving this node within the DOM causes a loss of // focus, we need to reapply the focus. diff --git a/core/field.ts b/core/field.ts index 815ee6f9e..ce34c8a87 100644 --- a/core/field.ts +++ b/core/field.ts @@ -361,7 +361,7 @@ export abstract class Field implements IASTNodeLocationSvg, if (!clickTarget) throw new Error('A click target has not been set.'); Tooltip.bindMouseEvents(clickTarget); this.mouseDownWrapper_ = browserEvents.conditionalBind( - clickTarget, 'mousedown', this, this.onMouseDown_); + clickTarget, 'pointerdown', this, this.onMouseDown_); } /** @@ -1076,11 +1076,11 @@ export abstract class Field implements IASTNodeLocationSvg, // NOP /** - * Handle a mouse down event on a field. + * Handle a pointerdown event on a field. * - * @param e Mouse down event. + * @param e Pointer down event. */ - protected onMouseDown_(e: Event) { + protected onMouseDown_(e: PointerEvent) { if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) { return; } diff --git a/core/field_angle.ts b/core/field_angle.ts index b44d3e327..448147c65 100644 --- a/core/field_angle.ts +++ b/core/field_angle.ts @@ -278,9 +278,9 @@ export class FieldAngle extends FieldInput { // a click handler on the drag surface to update the value if the surface // is clicked. this.clickSurfaceWrapper_ = browserEvents.conditionalBind( - circle, 'click', this, this.onMouseMove_, true, true); + circle, 'pointerdown', this, this.onMouseMove_, true); this.moveSurfaceWrapper_ = browserEvents.conditionalBind( - circle, 'mousemove', this, this.onMouseMove_, true, true); + circle, 'pointermove', this, this.onMouseMove_, true); this.editor_ = svg; } @@ -313,15 +313,11 @@ export class FieldAngle extends FieldInput { * * @param e Mouse move event. */ - protected onMouseMove_(e: Event) { + protected onMouseMove_(e: PointerEvent) { // Calculate angle. const bBox = this.gauge_!.ownerSVGElement!.getBoundingClientRect(); - // AnyDuringMigration because: Property 'clientX' does not exist on type - // 'Event'. - const dx = (e as AnyDuringMigration).clientX - bBox.left - FieldAngle.HALF; - // AnyDuringMigration because: Property 'clientY' does not exist on type - // 'Event'. - const dy = (e as AnyDuringMigration).clientY - bBox.top - FieldAngle.HALF; + const dx = e.clientX - bBox.left - FieldAngle.HALF; + const dy = e.clientY - bBox.top - FieldAngle.HALF; let angle = Math.atan(-dy / dx); if (isNaN(angle)) { // This shouldn't happen, but let's not let this error propagate further. diff --git a/core/field_colour.ts b/core/field_colour.ts index 3d7273b8b..729ebe54f 100644 --- a/core/field_colour.ts +++ b/core/field_colour.ts @@ -306,7 +306,7 @@ export class FieldColour extends Field { * * @param e Mouse event. */ - private onClick_(e: MouseEvent) { + private onClick_(e: PointerEvent) { const cell = e.target as Element; const colour = cell && cell.getAttribute('data-colour'); if (colour !== null) { @@ -415,7 +415,7 @@ export class FieldColour extends Field { * * @param e Mouse event. */ - private onMouseMove_(e: MouseEvent) { + private onMouseMove_(e: PointerEvent) { const cell = e.target as Element; const index = cell && Number(cell.getAttribute('data-index')); if (index !== null && index !== this.highlightedIndex_) { @@ -534,13 +534,13 @@ export class FieldColour extends Field { // Configure event handler on the table to listen for any event in a cell. this.onClickWrapper_ = browserEvents.conditionalBind( - table, 'click', this, this.onClick_, true); + table, 'pointerdown', this, this.onClick_, true); this.onMouseMoveWrapper_ = browserEvents.conditionalBind( - table, 'mousemove', this, this.onMouseMove_, true); + table, 'pointermove', this, this.onMouseMove_, true); this.onMouseEnterWrapper_ = browserEvents.conditionalBind( - table, 'mouseenter', this, this.onMouseEnter_, true); + table, 'pointerenter', this, this.onMouseEnter_, true); this.onMouseLeaveWrapper_ = browserEvents.conditionalBind( - table, 'mouseleave', this, this.onMouseLeave_, true); + table, 'pointerleave', this, this.onMouseLeave_, true); this.onKeyDownWrapper_ = browserEvents.conditionalBind(table, 'keydown', this, this.onKeyDown_); diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 89123a083..be6b5da4b 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -371,7 +371,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { Array.prototype.push.apply( this.eventWrappers_, browserEvents.conditionalBind( - (this.svgBackground_ as SVGPathElement), 'mousedown', this, + (this.svgBackground_ as SVGPathElement), 'pointerdown', this, this.onMouseDown_)); // A flyout connected to a workspace doesn't have its own current gesture. @@ -614,7 +614,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { } this.listeners_.push(browserEvents.conditionalBind( - (this.svgBackground_ as SVGPathElement), 'mouseover', this, + (this.svgBackground_ as SVGPathElement), 'pointerover', this, deselectAll)); if (this.horizontalLayout) { @@ -911,27 +911,27 @@ export abstract class Flyout extends DeleteArea implements IFlyout { protected addBlockListeners_( root: SVGElement, block: BlockSvg, rect: SVGElement) { this.listeners_.push(browserEvents.conditionalBind( - root, 'mousedown', null, this.blockMouseDown_(block))); + root, 'pointerdown', null, this.blockMouseDown_(block))); this.listeners_.push(browserEvents.conditionalBind( - rect, 'mousedown', null, this.blockMouseDown_(block))); + rect, 'pointerdown', null, this.blockMouseDown_(block))); this.listeners_.push( - browserEvents.bind(root, 'mouseenter', block, block.addSelect)); + browserEvents.bind(root, 'pointerenter', block, block.addSelect)); this.listeners_.push( - browserEvents.bind(root, 'mouseleave', block, block.removeSelect)); + browserEvents.bind(root, 'pointerleave', block, block.removeSelect)); this.listeners_.push( - browserEvents.bind(rect, 'mouseenter', block, block.addSelect)); + browserEvents.bind(rect, 'pointerenter', block, block.addSelect)); this.listeners_.push( - browserEvents.bind(rect, 'mouseleave', block, block.removeSelect)); + browserEvents.bind(rect, 'pointerleave', block, block.removeSelect)); } /** - * Handle a mouse-down on an SVG block in a non-closing flyout. + * Handle a pointerdown on an SVG block in a non-closing flyout. * * @param block The flyout block to copy. * @returns Function to call when block is clicked. */ private blockMouseDown_(block: BlockSvg): Function { - return (e: MouseEvent) => { + return (e: PointerEvent) => { const gesture = this.targetWorkspace.getGesture(e); if (gesture) { gesture.setStartBlock(block); @@ -941,11 +941,11 @@ export abstract class Flyout extends DeleteArea implements IFlyout { } /** - * Mouse down on the flyout background. Start a vertical scroll drag. + * Pointer down on the flyout background. Start a vertical scroll drag. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private onMouseDown_(e: MouseEvent) { + private onMouseDown_(e: PointerEvent) { const gesture = this.targetWorkspace.getGesture(e); if (gesture) { gesture.handleFlyoutStart(e, this); @@ -1026,7 +1026,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { // Clicking on a flyout button or label is a lot like clicking on the // flyout background. this.listeners_.push(browserEvents.conditionalBind( - buttonSvg, 'mousedown', this, this.onMouseDown_)); + buttonSvg, 'pointerdown', this, this.onMouseDown_)); this.buttons_.push(button); } diff --git a/core/flyout_button.ts b/core/flyout_button.ts index 256df77fd..9faa5671e 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -170,7 +170,8 @@ export class FlyoutButton { // AnyDuringMigration because: Argument of type 'SVGGElement | null' is not // assignable to parameter of type 'EventTarget'. this.onMouseUpWrapper_ = browserEvents.conditionalBind( - this.svgGroup_ as AnyDuringMigration, 'mouseup', this, this.onMouseUp_); + this.svgGroup_ as AnyDuringMigration, 'pointerup', this, + this.onMouseUp_); return this.svgGroup_!; } @@ -244,9 +245,9 @@ export class FlyoutButton { /** * Do something when the button is clicked. * - * @param e Mouse up event. + * @param e Pointer up event. */ - private onMouseUp_(e: Event) { + private onMouseUp_(e: PointerEvent) { const gesture = this.targetWorkspace.getGesture(e); if (gesture) { gesture.cancel(); diff --git a/core/gesture.ts b/core/gesture.ts index 295d30721..c9578f45e 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -5,8 +5,8 @@ */ /** - * The class representing an in-progress gesture, usually a drag - * or a tap. + * The class representing an in-progress gesture, e.g. a drag, + * tap, or pinch to zoom. * * @class */ @@ -38,10 +38,16 @@ import type {WorkspaceSvg} from './workspace_svg.js'; /** - * Note: In this file "start" refers to touchstart, mousedown, and pointerstart - * events. "End" refers to touchend, mouseup, and pointerend events. + * Note: In this file "start" refers to pointerdown + * events. "End" refers to pointerup events. */ -// TODO: Consider touchcancel/pointercancel. + +/** A multiplier used to convert the gesture scale to a zoom in delta. */ +const ZOOM_IN_MULTIPLIER = 5; + +/** A multiplier used to convert the gesture scale to a zoom out delta. */ +const ZOOM_OUT_MULTIPLIER = 6; + /** * Class for one gesture. * @@ -49,8 +55,8 @@ import type {WorkspaceSvg} from './workspace_svg.js'; */ export class Gesture { /** - * The position of the mouse when the gesture started. Units are CSS - * pixels, with (0, 0) at the top left of the browser window (mouseEvent + * The position of the pointer when the gesture started. Units are CSS + * pixels, with (0, 0) at the top left of the browser window (pointer event * clientX/Y). */ private mouseDownXY_ = new Coordinate(0, 0); @@ -97,13 +103,13 @@ export class Gesture { private hasExceededDragRadius_ = false; /** - * A handle to use to unbind a mouse move listener at the end of a drag. + * A handle to use to unbind a pointermove listener at the end of a drag. * Opaque data returned from Blockly.bindEventWithChecks_. */ protected onMoveWrapper_: browserEvents.Data|null = null; /** - * A handle to use to unbind a mouse up listener at the end of a drag. + * A handle to use to unbind a pointerup listener at the end of a drag. * Opaque data returned from Blockly.bindEventWithChecks_. */ protected onUpWrapper_: browserEvents.Data|null = null; @@ -134,18 +140,46 @@ export class Gesture { private healStack_: boolean; /** The event that most recently updated this gesture. */ - private mostRecentEvent_: Event; + private mostRecentEvent_: PointerEvent; + + /** Boolean for whether or not this gesture is a multi-touch gesture. */ + private isMultiTouch_ = false; + + /** A map of cached points used for tracking multi-touch gestures. */ + private cachedPoints = new Map(); + + /** + * This is the ratio between the starting distance between the touch points + * and the most recent distance between the touch points. + * Scales between 0 and 1 mean the most recent zoom was a zoom out. + * Scales above 1.0 mean the most recent zoom was a zoom in. + */ + private previousScale_ = 0; + + /** The starting distance between two touch points. */ + private startDistance_ = 0; + + /** + * A handle to use to unbind the second pointerdown listener + * at the end of a drag. + * Opaque data returned from Blockly.bindEventWithChecks_. + */ + private onStartWrapper_: browserEvents.Data|null = null; + + /** Boolean for whether or not the workspace supports pinch-zoom. */ + private isPinchZoomEnabled_: boolean|null = null; /** * @param e The event that kicked off this gesture. * @param creatorWorkspace The workspace that created this gesture and has a * reference to it. */ - constructor(e: Event, private readonly creatorWorkspace: WorkspaceSvg) { + constructor( + e: PointerEvent, private readonly creatorWorkspace: WorkspaceSvg) { this.mostRecentEvent_ = e; /** - * How far the mouse has moved during this drag, in pixel units. + * How far the pointer has moved during this drag, in pixel units. * (0, 0) is at this.mouseDownXY_. */ this.currentDragDeltaXY_ = new Coordinate(0, 0); @@ -181,19 +215,19 @@ export class Gesture { if (this.workspaceDragger_) { this.workspaceDragger_.dispose(); } + + if (this.onStartWrapper_) { + browserEvents.unbind(this.onStartWrapper_); + } } /** * Update internal state based on an event. * - * @param e The most recent mouse or touch event. + * @param e The most recent pointer event. */ - private updateFromEvent_(e: Event) { - // AnyDuringMigration because: Property 'clientY' does not exist on type - // 'Event'. AnyDuringMigration because: Property 'clientX' does not exist - // on type 'Event'. - const currentXY = new Coordinate( - (e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY); + private updateFromEvent_(e: PointerEvent) { + const currentXY = new Coordinate(e.clientX, e.clientY); const changed = this.updateDragDelta_(currentXY); // Exceeded the drag radius for the first time. if (changed) { @@ -204,9 +238,10 @@ export class Gesture { } /** - * DO MATH to set currentDragDeltaXY_ based on the most recent mouse position. + * DO MATH to set currentDragDeltaXY_ based on the most recent pointer + * position. * - * @param currentXY The most recent mouse/pointer position, in pixel units, + * @param currentXY The most recent pointer position, in pixel units, * with (0, 0) at the window's top left corner. * @returns True if the drag just exceeded the drag radius for the first time. */ @@ -230,7 +265,7 @@ export class Gesture { /** * Update this gesture to record whether a block is being dragged from the * flyout. - * This function should be called on a mouse/touch move event the first time + * This function should be called on a pointermove event the first time * the drag radius is exceeded. It should be called no more than once per * gesture. If a block should be dragged from the flyout this function creates * the new block on the main workspace and updates targetBlock_ and @@ -267,7 +302,7 @@ export class Gesture { /** * Update this gesture to record whether a bubble is being dragged. - * This function should be called on a mouse/touch move event the first time + * This function should be called on a pointermove event the first time * the drag radius is exceeded. It should be called no more than once per * gesture. If a bubble should be dragged this function creates the necessary * BubbleDragger and starts the drag. @@ -288,7 +323,7 @@ export class Gesture { * from the flyout or in the workspace, create the necessary BlockDragger and * start the drag. * - * This function should be called on a mouse/touch move event the first time + * This function should be called on a pointermove event the first time * the drag radius is exceeded. It should be called no more than once per * gesture. If a block should be dragged, either from the flyout or in the * workspace, this function creates the necessary BlockDragger and starts the @@ -316,7 +351,7 @@ export class Gesture { * Check whether to start a workspace drag. If a workspace is being dragged, * create the necessary WorkspaceDragger and start the drag. * - * This function should be called on a mouse/touch move event the first time + * This function should be called on a pointermove event the first time * the drag radius is exceeded. It should be called no more than once per * gesture. If a workspace is being dragged this function creates the * necessary WorkspaceDragger and starts the drag. @@ -340,7 +375,7 @@ export class Gesture { /** * Update this gesture to record whether anything is being dragged. - * This function should be called on a mouse/touch move event the first time + * This function should be called on a pointermove event the first time * the drag radius is exceeded. It should be called no more than once per * gesture. */ @@ -398,23 +433,25 @@ export class Gesture { /** * Start a gesture: update the workspace to indicate that a gesture is in - * progress and bind mousemove and mouseup handlers. + * progress and bind pointermove and pointerup handlers. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @internal */ - doStart(e: MouseEvent) { + doStart(e: PointerEvent) { + if (!this.startWorkspace_) { + throw new Error( + 'Cannot start the touch gesture becauase the start ' + + 'workspace is undefined'); + } + this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions && + this.startWorkspace_.options.zoomOptions.pinch; + if (browserEvents.isTargetInput(e)) { this.cancel(); return; } - if (!this.startWorkspace_) { - throw new Error( - 'Cannot start the gesture because the start ' + - 'workspace is undefined'); - } - this.hasStarted_ = true; blockAnimations.disconnectUiStop(); @@ -443,106 +480,252 @@ export class Gesture { return; } - // TODO(#6097): Make types accurate, possibly by refactoring touch handling. - const typelessEvent = e as AnyDuringMigration; - if ((e.type.toLowerCase() === 'touchstart' || - e.type.toLowerCase() === 'pointerdown') && - typelessEvent.pointerType !== 'mouse') { - Touch.longStart(typelessEvent, this); + if (e.type.toLowerCase() === 'pointerdown' && e.pointerType !== 'mouse') { + Touch.longStart(e, this); } - // AnyDuringMigration because: Property 'clientY' does not exist on type - // 'Event'. AnyDuringMigration because: Property 'clientX' does not exist - // on type 'Event'. - this.mouseDownXY_ = new Coordinate( - (e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY); - // AnyDuringMigration because: Property 'metaKey' does not exist on type - // 'Event'. AnyDuringMigration because: Property 'ctrlKey' does not exist - // on type 'Event'. AnyDuringMigration because: Property 'altKey' does not - // exist on type 'Event'. - this.healStack_ = (e as AnyDuringMigration).altKey || - (e as AnyDuringMigration).ctrlKey || (e as AnyDuringMigration).metaKey; + this.mouseDownXY_ = new Coordinate(e.clientX, e.clientY); + this.healStack_ = e.altKey || e.ctrlKey || e.metaKey; this.bindMouseEvents(e); + + if (!this.isEnding_) { + this.handleTouchStart(e); + } } /** * Bind gesture events. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @internal */ - bindMouseEvents(e: Event) { + bindMouseEvents(e: PointerEvent) { + this.onStartWrapper_ = browserEvents.conditionalBind( + document, 'pointerdown', null, this.handleStart.bind(this), + /* opt_noCaptureIdentifier */ true); this.onMoveWrapper_ = browserEvents.conditionalBind( - document, 'mousemove', null, this.handleMove.bind(this)); + document, 'pointermove', null, this.handleMove.bind(this), + /* opt_noCaptureIdentifier */ true); this.onUpWrapper_ = browserEvents.conditionalBind( - document, 'mouseup', null, this.handleUp.bind(this)); + document, 'pointerup', null, this.handleUp.bind(this), + /* opt_noCaptureIdentifier */ true); e.preventDefault(); e.stopPropagation(); } /** - * Handle a mouse move or touch move event. + * Handle a pointerdown event. * - * @param e A mouse move or touch move event. + * @param e A pointerdown event. * @internal */ - handleMove(e: Event) { - this.updateFromEvent_(e); - if (this.workspaceDragger_) { - this.workspaceDragger_.drag(this.currentDragDeltaXY_); - } else if (this.blockDragger_) { - this.blockDragger_.drag(this.mostRecentEvent_, this.currentDragDeltaXY_); - } else if (this.bubbleDragger_) { - this.bubbleDragger_.dragBubble( - this.mostRecentEvent_, this.currentDragDeltaXY_); - } - e.preventDefault(); - e.stopPropagation(); - } - - /** - * Handle a mouse up or touch end event. - * - * @param e A mouse up or touch end event. - * @internal - */ - handleUp(e: Event) { - this.updateFromEvent_(e); - Touch.longStop(); - - if (this.isEnding_) { - console.log('Trying to end a gesture recursively.'); + handleStart(e: PointerEvent) { + if (this.isDragging()) { + // A drag has already started, so this can no longer be a pinch-zoom. return; } - this.isEnding_ = true; - // The ordering of these checks is important: drags have higher priority - // than clicks. Fields have higher priority than blocks; blocks have higher - // priority than workspaces. - // The ordering within drags does not matter, because the three types of - // dragging are exclusive. - if (this.bubbleDragger_) { - this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_); - } else if (this.blockDragger_) { - this.blockDragger_.endDrag(e, this.currentDragDeltaXY_); - } else if (this.workspaceDragger_) { - this.workspaceDragger_.endDrag(this.currentDragDeltaXY_); - } else if (this.isBubbleClick_()) { - // Bubbles are in front of all fields and blocks. - this.doBubbleClick_(); - } else if (this.isFieldClick_()) { - this.doFieldClick_(); - } else if (this.isBlockClick_()) { - this.doBlockClick_(); - } else if (this.isWorkspaceClick_()) { - this.doWorkspaceClick_(e); + this.handleTouchStart(e); + + if (this.isMultiTouch()) { + Touch.longStop(); } + } + /** + * Handle a pointermove event. + * + * @param e A pointermove event. + * @internal + */ + handleMove(e: PointerEvent) { + if ((this.isDragging() && Touch.shouldHandleEvent(e)) || + !this.isMultiTouch()) { + this.updateFromEvent_(e); + if (this.workspaceDragger_) { + this.workspaceDragger_.drag(this.currentDragDeltaXY_); + } else if (this.blockDragger_) { + this.blockDragger_.drag( + this.mostRecentEvent_, this.currentDragDeltaXY_); + } else if (this.bubbleDragger_) { + this.bubbleDragger_.dragBubble( + this.mostRecentEvent_, this.currentDragDeltaXY_); + } + e.preventDefault(); + e.stopPropagation(); + } else if (this.isMultiTouch()) { + this.handleTouchMove(e); + Touch.longStop(); + } + } + + /** + * Handle a pointerup event. + * + * @param e A pointerup event. + * @internal + */ + handleUp(e: PointerEvent) { + if (!this.isDragging()) { + this.handleTouchEnd(e); + } + if (!this.isMultiTouch() || this.isDragging()) { + if (!Touch.shouldHandleEvent(e)) { + return; + } + this.updateFromEvent_(e); + Touch.longStop(); + + if (this.isEnding_) { + console.log('Trying to end a gesture recursively.'); + return; + } + this.isEnding_ = true; + // The ordering of these checks is important: drags have higher priority + // than clicks. Fields have higher priority than blocks; blocks have + // higher priority than workspaces. The ordering within drags does not + // matter, because the three types of dragging are exclusive. + if (this.bubbleDragger_) { + this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_); + } else if (this.blockDragger_) { + this.blockDragger_.endDrag(e, this.currentDragDeltaXY_); + } else if (this.workspaceDragger_) { + this.workspaceDragger_.endDrag(this.currentDragDeltaXY_); + } else if (this.isBubbleClick_()) { + // Bubbles are in front of all fields and blocks. + this.doBubbleClick_(); + } else if (this.isFieldClick_()) { + this.doFieldClick_(); + } else if (this.isBlockClick_()) { + this.doBlockClick_(); + } else if (this.isWorkspaceClick_()) { + this.doWorkspaceClick_(e); + } + + e.preventDefault(); + e.stopPropagation(); + + this.dispose(); + } else { + e.preventDefault(); + e.stopPropagation(); + + this.dispose(); + } + } + + /** + * Handle a pointerdown event and keep track of current + * pointers. + * + * @param e A pointerdown event. + * @internal + */ + handleTouchStart(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + // store the pointerId in the current list of pointers + this.cachedPoints.set(pointerId, this.getTouchPoint(e)); + const pointers = Array.from(this.cachedPoints.keys()); + // If two pointers are down, store info + if (pointers.length === 2) { + const point0 = (this.cachedPoints.get(pointers[0]))!; + const point1 = (this.cachedPoints.get(pointers[1]))!; + this.startDistance_ = Coordinate.distance(point0, point1); + this.isMultiTouch_ = true; + e.preventDefault(); + } + } + + /** + * Handle a pointermove event and zoom in/out if two pointers + * are on the screen. + * + * @param e A pointermove event. + * @internal + */ + handleTouchMove(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + // Update the cache + this.cachedPoints.set(pointerId, this.getTouchPoint(e)); + + if (this.isPinchZoomEnabled_ && this.cachedPoints.size === 2) { + this.handlePinch_(e); + } else { + this.handleMove(e); + } + } + + /** + * Handle pinch zoom gesture. + * + * @param e A pointermove event. + */ + private handlePinch_(e: PointerEvent) { + const pointers = Array.from(this.cachedPoints.keys()); + // Calculate the distance between the two pointers + const point0 = (this.cachedPoints.get(pointers[0]))!; + const point1 = (this.cachedPoints.get(pointers[1]))!; + const moveDistance = Coordinate.distance(point0, point1); + const scale = moveDistance / this.startDistance_; + + if (this.previousScale_ > 0 && this.previousScale_ < Infinity) { + const gestureScale = scale - this.previousScale_; + const delta = gestureScale > 0 ? gestureScale * ZOOM_IN_MULTIPLIER : + gestureScale * ZOOM_OUT_MULTIPLIER; + if (!this.startWorkspace_) { + throw new Error( + 'Cannot handle a pinch because the start workspace ' + + 'is undefined'); + } + const workspace = this.startWorkspace_; + const position = browserEvents.mouseToSvg( + e, workspace.getParentSvg(), workspace.getInverseScreenCTM()); + workspace.zoom(position.x, position.y, delta); + } + this.previousScale_ = scale; e.preventDefault(); - e.stopPropagation(); + } - this.dispose(); + /** + * Handle a pointerup event and end the gesture. + * + * @param e A pointerup event. + * @internal + */ + handleTouchEnd(e: PointerEvent) { + const pointerId = Touch.getTouchIdentifierFromEvent(e); + if (this.cachedPoints.has(pointerId)) { + this.cachedPoints.delete(pointerId); + } + if (this.cachedPoints.size < 2) { + this.cachedPoints.clear(); + this.previousScale_ = 0; + } + } + + /** + * Helper function returning the current touch point coordinate. + * + * @param e A pointer event. + * @returns The current touch point coordinate + * @internal + */ + getTouchPoint(e: PointerEvent): Coordinate|null { + if (!this.startWorkspace_) { + return null; + } + return new Coordinate(e.pageX, e.pageY); + } + + /** + * Whether this gesture is part of a multi-touch gesture. + * + * @returns Whether this gesture is part of a multi-touch gesture. + * @internal + */ + isMultiTouch(): boolean { + return this.isMultiTouch_; } /** @@ -574,10 +757,10 @@ export class Gesture { /** * Handle a real or faked right-click event by showing a context menu. * - * @param e A mouse move or touch move event. + * @param e A pointerdown event. * @internal */ - handleRightClick(e: Event) { + handleRightClick(e: PointerEvent) { if (this.targetBlock_) { this.bringBlockToFront_(); this.targetBlock_.workspace.hideChaff(!!this.flyout_); @@ -597,13 +780,13 @@ export class Gesture { } /** - * Handle a mousedown/touchstart event on a workspace. + * Handle a pointerdown event on a workspace. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @param ws The workspace the event hit. * @internal */ - handleWsStart(e: MouseEvent, ws: WorkspaceSvg) { + handleWsStart(e: PointerEvent, ws: WorkspaceSvg) { if (this.hasStarted_) { throw Error( 'Tried to call gesture.handleWsStart, ' + @@ -625,13 +808,13 @@ export class Gesture { } /** - * Handle a mousedown/touchstart event on a flyout. + * Handle a pointerdown event on a flyout. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @param flyout The flyout the event hit. * @internal */ - handleFlyoutStart(e: MouseEvent, flyout: IFlyout) { + handleFlyoutStart(e: PointerEvent, flyout: IFlyout) { if (this.hasStarted_) { throw Error( 'Tried to call gesture.handleFlyoutStart, ' + @@ -642,13 +825,13 @@ export class Gesture { } /** - * Handle a mousedown/touchstart event on a block. + * Handle a pointerdown event on a block. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @param block The block the event hit. * @internal */ - handleBlockStart(e: Event, block: BlockSvg) { + handleBlockStart(e: PointerEvent, block: BlockSvg) { if (this.hasStarted_) { throw Error( 'Tried to call gesture.handleBlockStart, ' + @@ -659,13 +842,13 @@ export class Gesture { } /** - * Handle a mousedown/touchstart event on a bubble. + * Handle a pointerdown event on a bubble. * - * @param e A mouse down or touch start event. + * @param e A pointerdown event. * @param bubble The bubble the event hit. * @internal */ - handleBubbleStart(e: Event, bubble: IBubble) { + handleBubbleStart(e: PointerEvent, bubble: IBubble) { if (this.hasStarted_) { throw Error( 'Tried to call gesture.handleBubbleStart, ' + @@ -734,9 +917,9 @@ export class Gesture { * Execute a workspace click. When in accessibility mode shift clicking will * move the cursor. * - * @param _e A mouse up or touch end event. + * @param _e A pointerup event. */ - private doWorkspaceClick_(_e: Event) { + private doWorkspaceClick_(_e: PointerEvent) { const ws = this.creatorWorkspace; if (common.getSelected()) { common.getSelected()!.unselect(); @@ -760,7 +943,7 @@ export class Gesture { } } - /* Begin functions for populating a gesture at mouse down. */ + /* Begin functions for populating a gesture at pointerdown. */ /** * Record the field that a gesture started on. @@ -849,14 +1032,14 @@ export class Gesture { } } - /* End functions for populating a gesture at mouse down. */ + /* End functions for populating a gesture at pointerdown. */ /* Begin helper functions defining types of clicks. Any developer wanting * to change the definition of a click should modify only this code. */ /** * Whether this gesture is a click on a bubble. This should only be called - * when ending a gesture (mouse up, touch end). + * when ending a gesture (pointerup). * * @returns Whether this gesture was a click on a bubble. */ @@ -868,7 +1051,7 @@ export class Gesture { /** * Whether this gesture is a click on a block. This should only be called - * when ending a gesture (mouse up, touch end). + * when ending a gesture (pointerup). * * @returns Whether this gesture was a click on a block. */ @@ -882,7 +1065,7 @@ export class Gesture { /** * Whether this gesture is a click on a field. This should only be called - * when ending a gesture (mouse up, touch end). + * when ending a gesture (pointerup). * * @returns Whether this gesture was a click on a field. */ @@ -895,7 +1078,7 @@ export class Gesture { /** * Whether this gesture is a click on a workspace. This should only be called - * when ending a gesture (mouse up, touch end). + * when ending a gesture (pointerup). * * @returns Whether this gesture was a click on a workspace. */ @@ -921,9 +1104,9 @@ export class Gesture { } /** - * Whether this gesture has already been started. In theory every mouse down - * has a corresponding mouse up, but in reality it is possible to lose a - * mouse up, leaving an in-process gesture hanging. + * Whether this gesture has already been started. In theory every pointerdown + * has a corresponding pointerup, but in reality it is possible to lose a + * pointerup, leaving an in-process gesture hanging. * * @returns Whether this gesture was a click on a workspace. * @internal diff --git a/core/icon.ts b/core/icon.ts index b771d706f..8cf0d6872 100644 --- a/core/icon.ts +++ b/core/icon.ts @@ -75,7 +75,7 @@ export abstract class Icon { this.getBlock().getSvgRoot().appendChild(this.iconGroup_); browserEvents.conditionalBind( - this.iconGroup_, 'mouseup', this, this.iconClick_); + this.iconGroup_, 'pointerup', this, this.iconClick_); this.updateEditable(); } @@ -104,7 +104,7 @@ export abstract class Icon { * * @param e Mouse click event. */ - protected iconClick_(e: MouseEvent) { + protected iconClick_(e: PointerEvent) { if (this.getBlock().workspace.isDragging()) { // Drag operation is concluding. Don't open the editor. return; diff --git a/core/inject.ts b/core/inject.ts index 833075665..71a359edb 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -393,7 +393,7 @@ function loadSounds(pathToMedia: string, workspace: WorkspaceSvg) { // Android ignores any sound not loaded as a result of a user action. soundBinds.push(browserEvents.conditionalBind( - document, 'mousemove', null, unbindSounds, true)); + document, 'pointermove', null, unbindSounds, true)); soundBinds.push(browserEvents.conditionalBind( document, 'touchstart', null, unbindSounds, true)); } diff --git a/core/menu.ts b/core/menu.ts index 93edeb1df..85b8070cf 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -105,13 +105,13 @@ export class Menu { // Add event handlers. this.mouseOverHandler = browserEvents.conditionalBind( - element, 'mouseover', this, this.handleMouseOver, true); + element, 'pointerover', this, this.handleMouseOver, true); this.clickHandler = browserEvents.conditionalBind( - element, 'click', this, this.handleClick, true); + element, 'pointerdown', this, this.handleClick, true); this.mouseEnterHandler = browserEvents.conditionalBind( - element, 'mouseenter', this, this.handleMouseEnter, true); + element, 'pointerenter', this, this.handleMouseEnter, true); this.mouseLeaveHandler = browserEvents.conditionalBind( - element, 'mouseleave', this, this.handleMouseLeave, true); + element, 'pointerleave', this, this.handleMouseLeave, true); this.onKeyDownHandler = browserEvents.conditionalBind( element, 'keydown', this, this.handleKeyEvent); @@ -310,7 +310,7 @@ export class Menu { * * @param e Mouse event to handle. */ - private handleMouseOver(e: Event) { + private handleMouseOver(e: PointerEvent) { const menuItem = this.getMenuItem(e.target as Element); if (menuItem) { @@ -329,18 +329,12 @@ export class Menu { * * @param e Click event to handle. */ - private handleClick(e: Event) { + private handleClick(e: PointerEvent) { const oldCoords = this.openingCoords; // Clear out the saved opening coords immediately so they're not used twice. this.openingCoords = null; - // AnyDuringMigration because: Property 'clientX' does not exist on type - // 'Event'. - if (oldCoords && typeof (e as AnyDuringMigration).clientX === 'number') { - // AnyDuringMigration because: Property 'clientY' does not exist on type - // 'Event'. AnyDuringMigration because: Property 'clientX' does not exist - // on type 'Event'. - const newCoords = new Coordinate( - (e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY); + if (oldCoords && typeof e.clientX === 'number') { + const newCoords = new Coordinate(e.clientX, e.clientY); if (Coordinate.distance(oldCoords, newCoords) < 1) { // This menu was opened by a mousedown and we're handling the consequent // click event. The coords haven't changed, meaning this was the same @@ -362,7 +356,7 @@ export class Menu { * * @param _e Mouse event to handle. */ - private handleMouseEnter(_e: Event) { + private handleMouseEnter(_e: PointerEvent) { this.focus(); } @@ -371,7 +365,7 @@ export class Menu { * * @param _e Mouse event to handle. */ - private handleMouseLeave(_e: Event) { + private handleMouseLeave(_e: PointerEvent) { if (this.getElement()) { this.blur(); this.setHighlighted(null); diff --git a/core/mutator.ts b/core/mutator.ts index 498608967..6153009b0 100644 --- a/core/mutator.ts +++ b/core/mutator.ts @@ -155,7 +155,7 @@ export class Mutator extends Icon { * * @param e Mouse click event. */ - protected override iconClick_(e: MouseEvent) { + protected override iconClick_(e: PointerEvent) { if (this.getBlock().isEditable()) { super.iconClick_(e); } diff --git a/core/scrollbar.ts b/core/scrollbar.ts index 313148852..652d5ae2d 100644 --- a/core/scrollbar.ts +++ b/core/scrollbar.ts @@ -211,9 +211,9 @@ export class Scrollbar { } this.onMouseDownBarWrapper_ = browserEvents.conditionalBind( - this.svgBackground, 'mousedown', this, this.onMouseDownBar); + this.svgBackground, 'pointerdown', this, this.onMouseDownBar); this.onMouseDownHandleWrapper_ = browserEvents.conditionalBind( - this.svgHandle, 'mousedown', this, this.onMouseDownHandle); + this.svgHandle, 'pointerdown', this, this.onMouseDownHandle); } /** @@ -703,7 +703,7 @@ export class Scrollbar { * * @param e Mouse down event. */ - private onMouseDownHandle(e: MouseEvent) { + private onMouseDownHandle(e: PointerEvent) { this.workspace.markFocused(); this.cleanUp(); if (browserEvents.isRightButton(e)) { @@ -723,9 +723,9 @@ export class Scrollbar { // Record the current mouse position. this.startDragMouse = this.horizontal ? e.clientX : e.clientY; this.onMouseUpWrapper_ = browserEvents.conditionalBind( - document, 'mouseup', this, this.onMouseUpHandle); + document, 'pointerup', this, this.onMouseUpHandle); this.onMouseMoveWrapper_ = browserEvents.conditionalBind( - document, 'mousemove', this, this.onMouseMoveHandle); + document, 'pointermove', this, this.onMouseMoveHandle); e.stopPropagation(); e.preventDefault(); } @@ -735,7 +735,7 @@ export class Scrollbar { * * @param e Mouse move event. */ - private onMouseMoveHandle(e: MouseEvent) { + private onMouseMoveHandle(e: PointerEvent) { const currentMouse = this.horizontal ? e.clientX : e.clientY; const mouseDelta = currentMouse - this.startDragMouse; const handlePosition = this.startDragHandle + mouseDelta; diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 3c9e3c23b..9b700fcf7 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -229,13 +229,13 @@ export class Toolbox extends DeleteArea implements IAutoHideable, container: HTMLDivElement, contentsContainer: HTMLDivElement) { // Clicking on toolbox closes popups. const clickEvent = browserEvents.conditionalBind( - container, 'click', this, this.onClick_, - /* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true); + container, 'pointerdown', this, this.onClick_, + /* opt_noCaptureIdentifier */ false); this.boundEvents_.push(clickEvent); const keyDownEvent = browserEvents.conditionalBind( contentsContainer, 'keydown', this, this.onKeyDown_, - /* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true); + /* opt_noCaptureIdentifier */ false); this.boundEvents_.push(keyDownEvent); } @@ -244,7 +244,7 @@ export class Toolbox extends DeleteArea implements IAutoHideable, * * @param e Click event to handle. */ - protected onClick_(e: MouseEvent) { + protected onClick_(e: PointerEvent) { if (browserEvents.isRightButton(e) || e.target === this.HtmlDiv) { // Close flyout. (common.getMainWorkspace() as WorkspaceSvg).hideChaff(false); diff --git a/core/tooltip.ts b/core/tooltip.ts index 498490ee2..cdd5bde0d 100644 --- a/core/tooltip.ts +++ b/core/tooltip.ts @@ -231,14 +231,14 @@ export function createDom() { export function bindMouseEvents(element: Element) { // TODO (#6097): Don't stash wrapper info on the DOM. (element as AnyDuringMigration).mouseOverWrapper_ = - browserEvents.bind(element, 'mouseover', null, onMouseOver); + browserEvents.bind(element, 'pointerover', null, onMouseOver); (element as AnyDuringMigration).mouseOutWrapper_ = - browserEvents.bind(element, 'mouseout', null, onMouseOut); + browserEvents.bind(element, 'pointerout', null, onMouseOut); // Don't use bindEvent_ for mousemove since that would create a // corresponding touch handler, even though this only makes sense in the // context of a mouseover/mouseout. - element.addEventListener('mousemove', onMouseMove, false); + element.addEventListener('pointermove', onMouseMove, false); } /** @@ -254,7 +254,7 @@ export function unbindMouseEvents(element: Element|null) { // TODO (#6097): Don't stash wrapper info on the DOM. browserEvents.unbind((element as AnyDuringMigration).mouseOverWrapper_); browserEvents.unbind((element as AnyDuringMigration).mouseOutWrapper_); - element.removeEventListener('mousemove', onMouseMove); + element.removeEventListener('pointermove', onMouseMove); } /** @@ -263,7 +263,7 @@ export function unbindMouseEvents(element: Element|null) { * * @param e Mouse event. */ -function onMouseOver(e: Event) { +function onMouseOver(e: PointerEvent) { if (blocked) { // Someone doesn't want us to show tooltips. return; @@ -285,7 +285,7 @@ function onMouseOver(e: Event) { * * @param _e Mouse event. */ -function onMouseOut(_e: Event) { +function onMouseOut(_e: PointerEvent) { if (blocked) { // Someone doesn't want us to show tooltips. return; diff --git a/core/touch.ts b/core/touch.ts index 2b621bac6..250b162a1 100644 --- a/core/touch.ts +++ b/core/touch.ts @@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.Touch'); import type {Gesture} from './gesture.js'; +import * as deprecation from './utils/deprecation.js'; /** @@ -52,23 +53,17 @@ let touchIdentifier_: string|null = null; * * @alias Blockly.Touch.TOUCH_MAP */ -export const TOUCH_MAP: {[key: string]: string[]} = globalThis['PointerEvent'] ? - { - 'mousedown': ['pointerdown'], - 'mouseenter': ['pointerenter'], - 'mouseleave': ['pointerleave'], - 'mousemove': ['pointermove'], - 'mouseout': ['pointerout'], - 'mouseover': ['pointerover'], - 'mouseup': ['pointerup', 'pointercancel'], - 'touchend': ['pointerup'], - 'touchcancel': ['pointercancel'], - } : - { - 'mousedown': ['touchstart'], - 'mousemove': ['touchmove'], - 'mouseup': ['touchend', 'touchcancel'], - }; +export const TOUCH_MAP: {[key: string]: string[]} = { + 'mousedown': ['pointerdown'], + 'mouseenter': ['pointerenter'], + 'mouseleave': ['pointerleave'], + 'mousemove': ['pointermove'], + 'mouseout': ['pointerout'], + 'mouseover': ['pointerover'], + 'mouseup': ['pointerup', 'pointercancel'], + 'touchend': ['pointerup'], + 'touchcancel': ['pointercancel'], +}; /** PID of queued long-press task. */ let longPid_: AnyDuringMigration = 0; @@ -85,29 +80,9 @@ let longPid_: AnyDuringMigration = 0; * @alias Blockly.Touch.longStart * @internal */ -export function longStart(e: Event, gesture: Gesture) { +export function longStart(e: PointerEvent, gesture: Gesture) { longStop(); - // Punt on multitouch events. - // AnyDuringMigration because: Property 'changedTouches' does not exist on - // type 'Event'. - if ((e as AnyDuringMigration).changedTouches && - (e as AnyDuringMigration).changedTouches.length !== 1) { - return; - } longPid_ = setTimeout(function() { - // TODO(#6097): Make types accurate, possibly by refactoring touch handling. - // AnyDuringMigration because: Property 'changedTouches' does not exist on - // type 'Event'. - const typelessEvent = e as AnyDuringMigration; - // Additional check to distinguish between touch events and pointer events - if (typelessEvent.changedTouches) { - // TouchEvent - typelessEvent.button = 2; // Simulate a right button click. - // e was a touch event. It needs to pretend to be a mouse event. - typelessEvent.clientX = typelessEvent.changedTouches[0].clientX; - typelessEvent.clientY = typelessEvent.changedTouches[0].clientY; - } - // Let the gesture route the right-click correctly. if (gesture) { gesture.handleRightClick(e); @@ -150,78 +125,46 @@ export function clearTouchIdentifier() { * handler; false if it should be blocked. * @alias Blockly.Touch.shouldHandleEvent */ -export function shouldHandleEvent(e: Event|PseudoEvent): boolean { - return !isMouseOrTouchEvent(e) || checkTouchIdentifier(e); +export function shouldHandleEvent(e: Event): boolean { + // Do not replace the startsWith with a check for `instanceof PointerEvent`. + // `click` and `contextmenu` are PointerEvents in some browsers, + // despite not starting with `pointer`, but we want to always handle them + // without worrying about touch identifiers. + return !(e.type.startsWith('pointer')) || + (e instanceof PointerEvent && checkTouchIdentifier(e)); } /** - * Get the touch identifier from the given event. If it was a mouse event, the - * identifier is the string 'mouse'. + * Get the pointer identifier from the given event. * - * @param e Pointer event, mouse event, or touch event. - * @returns The pointerId, or touch identifier from the first changed touch, if - * defined. Otherwise 'mouse'. + * @param e Pointer event. + * @returns The pointerId of the event. * @alias Blockly.Touch.getTouchIdentifierFromEvent */ -export function getTouchIdentifierFromEvent(e: Event|PseudoEvent): string { - if (e instanceof PointerEvent) { - return String(e.pointerId); - } - - if (e instanceof MouseEvent) { - return 'mouse'; - } - - /** - * TODO(#6097): Fix types. This is a catch-all for everything but mouse - * and pointer events. - */ - const pseudoEvent = /** {!PseudoEvent} */ e; - - // AnyDuringMigration because: Property 'changedTouches' does not exist on - // type 'PseudoEvent | Event'. AnyDuringMigration because: Property - // 'changedTouches' does not exist on type 'PseudoEvent | Event'. - // AnyDuringMigration because: Property 'changedTouches' does not exist on - // type 'PseudoEvent | Event'. AnyDuringMigration because: Property - // 'changedTouches' does not exist on type 'PseudoEvent | Event'. - // AnyDuringMigration because: Property 'changedTouches' does not exist on - // type 'PseudoEvent | Event'. - return (pseudoEvent as AnyDuringMigration).changedTouches && - (pseudoEvent as AnyDuringMigration).changedTouches[0] && - (pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !== - undefined && - (pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !== - null ? - String((pseudoEvent as AnyDuringMigration).changedTouches[0].identifier) : - 'mouse'; +export function getTouchIdentifierFromEvent(e: PointerEvent): string { + return `${e.pointerId}`; } /** - * Check whether the touch identifier on the event matches the current saved - * identifier. If there is no identifier, that means it's a mouse event and - * we'll use the identifier "mouse". This means we won't deal well with - * multiple mice being used at the same time. That seems okay. - * If the current identifier was unset, save the identifier from the - * event. This starts a drag/gesture, during which touch events with other - * identifiers will be silently ignored. + * Check whether the pointer identifier on the event matches the current saved + * identifier. If the current identifier was unset, save the identifier from + * the event. This starts a drag/gesture, during which pointer events with + * other identifiers will be silently ignored. * - * @param e Mouse event or touch event. + * @param e Pointer event. * @returns Whether the identifier on the event matches the current saved * identifier. * @alias Blockly.Touch.checkTouchIdentifier */ -export function checkTouchIdentifier(e: Event|PseudoEvent): boolean { +export function checkTouchIdentifier(e: PointerEvent): boolean { const identifier = getTouchIdentifierFromEvent(e); - // if (touchIdentifier_) is insufficient because Android touch - // identifiers may be zero. - if (touchIdentifier_ !== undefined && touchIdentifier_ !== null) { + if (touchIdentifier_) { // We're already tracking some touch/mouse event. Is this from the same // source? return touchIdentifier_ === identifier; } - if (e.type === 'mousedown' || e.type === 'touchstart' || - e.type === 'pointerdown') { + if (e.type === 'pointerdown') { // No identifier set yet, and this is the start of a drag. Set it and // return. touchIdentifier_ = identifier; @@ -241,6 +184,7 @@ export function checkTouchIdentifier(e: Event|PseudoEvent): boolean { * @alias Blockly.Touch.setClientFromTouch */ export function setClientFromTouch(e: Event|PseudoEvent) { + deprecation.warn('setClientFromTouch()', 'version 9', 'version 10'); // AnyDuringMigration because: Property 'changedTouches' does not exist on // type 'PseudoEvent | Event'. if (e.type.startsWith('touch') && (e as AnyDuringMigration).changedTouches) { @@ -265,6 +209,7 @@ export function setClientFromTouch(e: Event|PseudoEvent) { * @alias Blockly.Touch.isMouseOrTouchEvent */ export function isMouseOrTouchEvent(e: Event|PseudoEvent): boolean { + deprecation.warn('isMouseOrTouchEvent()', 'version 9', 'version 10'); return e.type.startsWith('touch') || e.type.startsWith('mouse') || e.type.startsWith('pointer'); } @@ -277,6 +222,7 @@ export function isMouseOrTouchEvent(e: Event|PseudoEvent): boolean { * @alias Blockly.Touch.isTouchEvent */ export function isTouchEvent(e: Event|PseudoEvent): boolean { + deprecation.warn('isTouchEvent()', 'version 9', 'version 10'); return e.type.startsWith('touch') || e.type.startsWith('pointer'); } @@ -291,6 +237,7 @@ export function isTouchEvent(e: Event|PseudoEvent): boolean { * @alias Blockly.Touch.splitEventByTouches */ export function splitEventByTouches(e: Event): Array { + deprecation.warn('splitEventByTouches()', 'version 9', 'version 10'); const events = []; // AnyDuringMigration because: Property 'changedTouches' does not exist on // type 'PseudoEvent | Event'. diff --git a/core/touch_gesture.ts b/core/touch_gesture.ts deleted file mode 100644 index 4fea2fdbd..000000000 --- a/core/touch_gesture.ts +++ /dev/null @@ -1,312 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * The class extends Gesture to support pinch to zoom - * for both pointer and touch events. - * - * @class - */ -import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.TouchGesture'); - -import * as browserEvents from './browser_events.js'; -import {Gesture} from './gesture.js'; -import * as Touch from './touch.js'; -import {Coordinate} from './utils/coordinate.js'; - -/* - * Note: In this file "start" refers to touchstart, mousedown, and pointerstart - * events. "End" refers to touchend, mouseup, and pointerend events. - */ - -/** A multiplier used to convert the gesture scale to a zoom in delta. */ -const ZOOM_IN_MULTIPLIER = 5; - -/** A multiplier used to convert the gesture scale to a zoom out delta. */ -const ZOOM_OUT_MULTIPLIER = 6; - -/** - * Class for one gesture. - * - * @alias Blockly.TouchGesture - */ -export class TouchGesture extends Gesture { - /** Boolean for whether or not this gesture is a multi-touch gesture. */ - private isMultiTouch_ = false; - - /** A map of cached points used for tracking multi-touch gestures. */ - private cachedPoints = new Map(); - - /** - * This is the ratio between the starting distance between the touch points - * and the most recent distance between the touch points. - * Scales between 0 and 1 mean the most recent zoom was a zoom out. - * Scales above 1.0 mean the most recent zoom was a zoom in. - */ - private previousScale_ = 0; - - /** The starting distance between two touch points. */ - private startDistance_ = 0; - - /** - * A handle to use to unbind the second touch start or pointer down listener - * at the end of a drag. - * Opaque data returned from Blockly.bindEventWithChecks_. - */ - private onStartWrapper_: browserEvents.Data|null = null; - - /** Boolean for whether or not the workspace supports pinch-zoom. */ - private isPinchZoomEnabled_: boolean|null = null; - override onMoveWrapper_: browserEvents.Data|null = null; - override onUpWrapper_: browserEvents.Data|null = null; - - /** - * Start a gesture: update the workspace to indicate that a gesture is in - * progress and bind mousemove and mouseup handlers. - * - * @param e A mouse down, touch start or pointer down event. - * @internal - */ - override doStart(e: MouseEvent) { - if (!this.startWorkspace_) { - throw new Error( - 'Cannot start the touch event becauase the start ' + - 'workspace is undefined'); - } - this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions && - this.startWorkspace_.options.zoomOptions.pinch; - super.doStart(e); - if (!this.isEnding_ && Touch.isTouchEvent(e)) { - this.handleTouchStart(e); - } - } - - /** - * Bind gesture events. - * Overriding the gesture definition of this function, binding the same - * functions for onMoveWrapper_ and onUpWrapper_ but passing - * opt_noCaptureIdentifier. - * In addition, binding a second mouse down event to detect multi-touch - * events. - * - * @param e A mouse down or touch start event. - * @internal - */ - override bindMouseEvents(e: Event) { - this.onStartWrapper_ = browserEvents.conditionalBind( - document, 'mousedown', null, this.handleStart.bind(this), - /* opt_noCaptureIdentifier */ true); - this.onMoveWrapper_ = browserEvents.conditionalBind( - document, 'mousemove', null, this.handleMove.bind(this), - /* opt_noCaptureIdentifier */ true); - this.onUpWrapper_ = browserEvents.conditionalBind( - document, 'mouseup', null, this.handleUp.bind(this), - /* opt_noCaptureIdentifier */ true); - - e.preventDefault(); - e.stopPropagation(); - } - - /** - * Handle a mouse down, touch start, or pointer down event. - * - * @param e A mouse down, touch start, or pointer down event. - * @internal - */ - handleStart(e: Event) { - if (this.isDragging()) { - // A drag has already started, so this can no longer be a pinch-zoom. - return; - } - if (Touch.isTouchEvent(e)) { - this.handleTouchStart(e); - - if (this.isMultiTouch()) { - Touch.longStop(); - } - } - } - - /** - * Handle a mouse move, touch move, or pointer move event. - * - * @param e A mouse move, touch move, or pointer move event. - * @internal - */ - override handleMove(e: MouseEvent) { - if (this.isDragging()) { - // We are in the middle of a drag, only handle the relevant events - if (Touch.shouldHandleEvent(e)) { - super.handleMove(e); - } - return; - } - if (this.isMultiTouch()) { - if (Touch.isTouchEvent(e)) { - this.handleTouchMove(e); - } - Touch.longStop(); - } else { - super.handleMove(e); - } - } - - /** - * Handle a mouse up, touch end, or pointer up event. - * - * @param e A mouse up, touch end, or pointer up event. - * @internal - */ - override handleUp(e: Event) { - if (Touch.isTouchEvent(e) && !this.isDragging()) { - this.handleTouchEnd(e); - } - if (!this.isMultiTouch() || this.isDragging()) { - if (!Touch.shouldHandleEvent(e)) { - return; - } - super.handleUp(e); - } else { - e.preventDefault(); - e.stopPropagation(); - - this.dispose(); - } - } - - /** - * Whether this gesture is part of a multi-touch gesture. - * - * @returns Whether this gesture is part of a multi-touch gesture. - * @internal - */ - isMultiTouch(): boolean { - return this.isMultiTouch_; - } - - /** - * Sever all links from this object. - * - * @internal - */ - override dispose() { - super.dispose(); - - if (this.onStartWrapper_) { - browserEvents.unbind(this.onStartWrapper_); - } - } - - /** - * Handle a touch start or pointer down event and keep track of current - * pointers. - * - * @param e A touch start, or pointer down event. - * @internal - */ - handleTouchStart(e: Event) { - const pointerId = Touch.getTouchIdentifierFromEvent(e); - // store the pointerId in the current list of pointers - this.cachedPoints.set(pointerId, this.getTouchPoint(e)); - const pointers = Array.from(this.cachedPoints.keys()); - // If two pointers are down, store info - if (pointers.length === 2) { - const point0 = (this.cachedPoints.get(pointers[0]))!; - const point1 = (this.cachedPoints.get(pointers[1]))!; - this.startDistance_ = Coordinate.distance(point0, point1); - this.isMultiTouch_ = true; - e.preventDefault(); - } - } - - /** - * Handle a touch move or pointer move event and zoom in/out if two pointers - * are on the screen. - * - * @param e A touch move, or pointer move event. - * @internal - */ - handleTouchMove(e: MouseEvent) { - const pointerId = Touch.getTouchIdentifierFromEvent(e); - // Update the cache - this.cachedPoints.set(pointerId, this.getTouchPoint(e)); - - if (this.isPinchZoomEnabled_ && this.cachedPoints.size === 2) { - this.handlePinch_(e); - } else { - super.handleMove(e); - } - } - - /** - * Handle pinch zoom gesture. - * - * @param e A touch move, or pointer move event. - */ - private handlePinch_(e: MouseEvent) { - const pointers = Array.from(this.cachedPoints.keys()); - // Calculate the distance between the two pointers - const point0 = (this.cachedPoints.get(pointers[0]))!; - const point1 = (this.cachedPoints.get(pointers[1]))!; - const moveDistance = Coordinate.distance(point0, point1); - const scale = moveDistance / this.startDistance_; - - if (this.previousScale_ > 0 && this.previousScale_ < Infinity) { - const gestureScale = scale - this.previousScale_; - const delta = gestureScale > 0 ? gestureScale * ZOOM_IN_MULTIPLIER : - gestureScale * ZOOM_OUT_MULTIPLIER; - if (!this.startWorkspace_) { - throw new Error( - 'Cannot handle a pinch because the start workspace ' + - 'is undefined'); - } - const workspace = this.startWorkspace_; - const position = browserEvents.mouseToSvg( - e, workspace.getParentSvg(), workspace.getInverseScreenCTM()); - workspace.zoom(position.x, position.y, delta); - } - this.previousScale_ = scale; - e.preventDefault(); - } - - /** - * Handle a touch end or pointer end event and end the gesture. - * - * @param e A touch end, or pointer end event. - * @internal - */ - handleTouchEnd(e: Event) { - const pointerId = Touch.getTouchIdentifierFromEvent(e); - if (this.cachedPoints.has(pointerId)) { - this.cachedPoints.delete(pointerId); - } - if (this.cachedPoints.size < 2) { - this.cachedPoints.clear(); - this.previousScale_ = 0; - } - } - - /** - * Helper function returning the current touch point coordinate. - * - * @param e A touch or pointer event. - * @returns The current touch point coordinate - * @internal - */ - getTouchPoint(e: Event): Coordinate|null { - if (!this.startWorkspace_) { - return null; - } - // TODO(#6097): Make types accurate, possibly by refactoring touch handling. - const typelessEvent = e as AnyDuringMigration; - return new Coordinate( - typelessEvent.changedTouches ? typelessEvent.changedTouches[0].pageX : - typelessEvent.pageX, - typelessEvent.changedTouches ? typelessEvent.changedTouches[0].pageY : - typelessEvent.pageY); - } -} diff --git a/core/trashcan.ts b/core/trashcan.ts index 18bd2e370..938dcf815 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -201,11 +201,11 @@ export class Trashcan extends DeleteArea implements IAutoHideable, // Using bindEventWithChecks_ for blocking mousedown causes issue in mobile. // See #4303 browserEvents.bind( - this.svgGroup_, 'mousedown', this, this.blockMouseDownWhenOpenable_); - browserEvents.bind(this.svgGroup_, 'mouseup', this, this.click); + this.svgGroup_, 'pointerdown', this, this.blockMouseDownWhenOpenable_); + browserEvents.bind(this.svgGroup_, 'pointerup', this, this.click); // Bind to body instead of this.svgGroup_ so that we don't get lid jitters - browserEvents.bind(body, 'mouseover', this, this.mouseOver_); - browserEvents.bind(body, 'mouseout', this, this.mouseOut_); + browserEvents.bind(body, 'pointerover', this, this.mouseOver_); + browserEvents.bind(body, 'pointerout', this, this.mouseOut_); this.animateLid_(); return this.svgGroup_; } @@ -513,7 +513,7 @@ export class Trashcan extends DeleteArea implements IAutoHideable, * * @param e A mouse down event. */ - private blockMouseDownWhenOpenable_(e: Event) { + private blockMouseDownWhenOpenable_(e: PointerEvent) { if (!this.contentsIsOpen() && this.hasContents_()) { // Don't start a workspace scroll. e.stopPropagation(); diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts index 1af608b41..9e5e96b0d 100644 --- a/core/workspace_comment_svg.ts +++ b/core/workspace_comment_svg.ts @@ -170,10 +170,10 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } if (!this.workspace.options.readOnly && !this.eventsInit_) { browserEvents.conditionalBind( - this.svgRectTarget_ as SVGRectElement, 'mousedown', this, + this.svgRectTarget_ as SVGRectElement, 'pointerdown', this, this.pathMouseDown_); browserEvents.conditionalBind( - this.svgHandleTarget_ as SVGRectElement, 'mousedown', this, + this.svgHandleTarget_ as SVGRectElement, 'pointerdown', this, this.pathMouseDown_); } this.eventsInit_ = true; @@ -189,11 +189,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Handle a mouse-down on an SVG comment. + * Handle a pointerdown on an SVG comment. * - * @param e Mouse down event or touch start event. + * @param e Pointer down event. */ - private pathMouseDown_(e: Event) { + private pathMouseDown_(e: PointerEvent) { const gesture = this.workspace.getGesture(e); if (gesture) { gesture.handleBubbleStart(e, this); @@ -203,11 +203,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements /** * Show the context menu for this workspace comment. * - * @param e Mouse event. + * @param e Pointer event. * @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - showContextMenu(e: Event) { + showContextMenu(e: PointerEvent) { throw new Error( 'The implementation of showContextMenu should be ' + 'monkey-patched in by blockly.ts'); @@ -685,18 +685,18 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements if (this.resizeGroup_) { browserEvents.conditionalBind( - (this.resizeGroup_), 'mousedown', this, this.resizeMouseDown_); + (this.resizeGroup_), 'pointerdown', this, this.resizeMouseDown_); } if (this.isDeletable()) { browserEvents.conditionalBind( - this.deleteGroup_ as SVGGElement, 'mousedown', this, + this.deleteGroup_ as SVGGElement, 'pointerdown', this, this.deleteMouseDown_); browserEvents.conditionalBind( - this.deleteGroup_ as SVGGElement, 'mouseout', this, + this.deleteGroup_ as SVGGElement, 'pointerout', this, this.deleteMouseOut_); browserEvents.conditionalBind( - this.deleteGroup_ as SVGGElement, 'mouseup', this, + this.deleteGroup_ as SVGGElement, 'pointerup', this, this.deleteMouseUp_); } } @@ -820,11 +820,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Handle a mouse-down on comment's resize corner. + * Handle a pointerdown on comment's resize corner. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private resizeMouseDown_(e: MouseEvent) { + private resizeMouseDown_(e: PointerEvent) { this.unbindDragEvents_(); if (browserEvents.isRightButton(e)) { // No right-click. @@ -838,20 +838,20 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements this.workspace.RTL ? -this.width_ : this.width_, this.height_)); this.onMouseUpWrapper_ = browserEvents.conditionalBind( - document, 'mouseup', this, this.resizeMouseUp_); + document, 'pointerup', this, this.resizeMouseUp_); this.onMouseMoveWrapper_ = browserEvents.conditionalBind( - document, 'mousemove', this, this.resizeMouseMove_); + document, 'pointermove', this, this.resizeMouseMove_); this.workspace.hideChaff(); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); } /** - * Handle a mouse-down on comment's delete icon. + * Handle a pointerdown on comment's delete icon. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private deleteMouseDown_(e: Event) { + private deleteMouseDown_(e: PointerEvent) { // Highlight the delete icon. if (this.deleteIconBorder_) { dom.addClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted'); @@ -861,11 +861,11 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Handle a mouse-out on comment's delete icon. + * Handle a pointerout on comment's delete icon. * - * @param _e Mouse out event. + * @param _e Pointer out event. */ - private deleteMouseOut_(_e: Event) { + private deleteMouseOut_(_e: PointerEvent) { // Restore highlight on the delete icon. if (this.deleteIconBorder_) { dom.removeClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted'); @@ -873,18 +873,18 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Handle a mouse-up on comment's delete icon. + * Handle a pointerup on comment's delete icon. * - * @param e Mouse up event. + * @param e Pointer up event. */ - private deleteMouseUp_(e: Event) { + private deleteMouseUp_(e: PointerEvent) { // Delete this comment. this.dispose(); // This event has been handled. No need to bubble up to the document. e.stopPropagation(); } - /** Stop binding to the global mouseup and mousemove events. */ + /** Stop binding to the global pointerup and pointermove events. */ private unbindDragEvents_() { if (this.onMouseUpWrapper_) { browserEvents.unbind(this.onMouseUpWrapper_); @@ -897,21 +897,22 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } /** - * Handle a mouse-up event while dragging a comment's border or resize handle. + * Handle a pointerup event while dragging a comment's border or resize + * handle. * - * @param _e Mouse up event. + * @param _e Pointer up event. */ - private resizeMouseUp_(_e: Event) { + private resizeMouseUp_(_e: PointerEvent) { Touch.clearTouchIdentifier(); this.unbindDragEvents_(); } /** - * Resize this comment to follow the mouse. + * Resize this comment to follow the pointer. * - * @param e Mouse move event. + * @param e Pointer move event. */ - private resizeMouseMove_(e: MouseEvent) { + private resizeMouseMove_(e: PointerEvent) { this.autoLayout_ = false; const newXY = this.workspace.moveDrag(e); this.setSize_(this.RTL ? -newXY.x : newXY.x, newXY.y); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index fb6a006d0..06c43d682 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -56,7 +56,6 @@ import type {Theme} from './theme.js'; import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; import * as Tooltip from './tooltip.js'; -import {TouchGesture} from './touch_gesture.js'; import type {Trashcan} from './trashcan.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; @@ -224,7 +223,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @internal */ - currentGesture_: TouchGesture|null = null; + currentGesture_: Gesture|null = null; /** This workspace's surface for dragging blocks, if it exists. */ private readonly blockDragSurface: BlockDragSurfaceSvg|null = null; @@ -774,7 +773,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { if (!this.isFlyout) { browserEvents.conditionalBind( - this.svgGroup_, 'mousedown', this, this.onMouseDown_, false, true); + this.svgGroup_, 'pointerdown', this, this.onMouseDown_, false); // This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683, // which otherwise prevents zoom/scroll events from being observed in // Safari. Once that bug is fixed it should be removed. @@ -1594,17 +1593,15 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /* eslint-enable */ /** - * Returns the drag target the mouse event is over. + * Returns the drag target the pointer event is over. * - * @param e Mouse move event. + * @param e Pointer move event. * @returns Null if not over a drag target, or the drag target the event is * over. */ - getDragTarget(e: Event): IDragTarget|null { + getDragTarget(e: PointerEvent): IDragTarget|null { for (let i = 0, targetArea; targetArea = this.dragTargetAreas[i]; i++) { - if (targetArea.clientRect.contains( - (e as AnyDuringMigration).clientX, - (e as AnyDuringMigration).clientY)) { + if (targetArea.clientRect.contains(e.clientX, e.clientY)) { return targetArea.component; } } @@ -1612,11 +1609,11 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Handle a mouse-down on SVG drawing surface. + * Handle a pointerdown on SVG drawing surface. * - * @param e Mouse down event. + * @param e Pointer down event. */ - private onMouseDown_(e: MouseEvent) { + private onMouseDown_(e: PointerEvent) { const gesture = this.getGesture(e); if (gesture) { gesture.handleWsStart(e, this); @@ -1626,10 +1623,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * Start tracking a drag of an object on this workspace. * - * @param e Mouse down event. + * @param e Pointer down event. * @param xy Starting location of object. */ - startDrag(e: MouseEvent, xy: Coordinate) { + startDrag(e: PointerEvent, xy: Coordinate) { // Record the starting offset between the bubble's location and the mouse. const point = browserEvents.mouseToSvg( e, this.getParentSvg(), this.getInverseScreenCTM()); @@ -1642,10 +1639,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * Track a drag of an object on this workspace. * - * @param e Mouse move event. + * @param e Pointer move event. * @returns New location of object. */ - moveDrag(e: MouseEvent): Coordinate { + moveDrag(e: PointerEvent): Coordinate { const point = browserEvents.mouseToSvg( e, this.getParentSvg(), this.getInverseScreenCTM()); // Fix scale of mouse event. @@ -2471,14 +2468,13 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * Look up the gesture that is tracking this touch stream on this workspace. * May create a new gesture. * - * @param e Mouse event or touch event. + * @param e Pointer event. * @returns The gesture that is tracking this touch stream, or null if no * valid gesture exists. * @internal */ - getGesture(e: Event): TouchGesture|null { - const isStart = e.type === 'mousedown' || e.type === 'touchstart' || - e.type === 'pointerdown'; + getGesture(e: PointerEvent): Gesture|null { + const isStart = e.type === 'pointerdown'; const gesture = this.currentGesture_; if (gesture) { @@ -2495,7 +2491,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // No gesture existed on this workspace, but this looks like the start of a // new gesture. if (isStart) { - this.currentGesture_ = new TouchGesture(e, this); + this.currentGesture_ = new Gesture(e, this); return this.currentGesture_; } // No gesture existed and this event couldn't be the start of a new gesture. diff --git a/core/zoom_controls.ts b/core/zoom_controls.ts index 6b4f6f4b2..b25ff4a85 100644 --- a/core/zoom_controls.ts +++ b/core/zoom_controls.ts @@ -276,7 +276,7 @@ export class ZoomControls implements IPositionable { // Attach listener. this.onZoomOutWrapper = browserEvents.conditionalBind( - this.zoomOutGroup, 'mousedown', null, this.zoom.bind(this, -1)); + this.zoomOutGroup, 'pointerdown', null, this.zoom.bind(this, -1)); } /** @@ -322,7 +322,7 @@ export class ZoomControls implements IPositionable { // Attach listener. this.onZoomInWrapper = browserEvents.conditionalBind( - this.zoomInGroup, 'mousedown', null, this.zoom.bind(this, 1)); + this.zoomInGroup, 'pointerdown', null, this.zoom.bind(this, 1)); } /** @@ -333,7 +333,7 @@ export class ZoomControls implements IPositionable { * positive amount values zoom in. * @param e A mouse down event. */ - private zoom(amount: number, e: Event) { + private zoom(amount: number, e: PointerEvent) { this.workspace.markFocused(); this.workspace.zoomCenter(amount); this.fireZoomEvent(); @@ -380,7 +380,7 @@ export class ZoomControls implements IPositionable { // Attach event listeners. this.onZoomResetWrapper = browserEvents.conditionalBind( - this.zoomResetGroup, 'mousedown', null, this.resetZoom.bind(this)); + this.zoomResetGroup, 'pointerdown', null, this.resetZoom.bind(this)); } /** @@ -388,7 +388,7 @@ export class ZoomControls implements IPositionable { * * @param e A mouse down event. */ - private resetZoom(e: Event) { + private resetZoom(e: PointerEvent) { this.workspace.markFocused(); // zoom is passed amount and computes the new scale using the formula: diff --git a/scripts/migration/renamings.json5 b/scripts/migration/renamings.json5 index f5d0e858e..2c33d67ef 100644 --- a/scripts/migration/renamings.json5 +++ b/scripts/migration/renamings.json5 @@ -1424,5 +1424,9 @@ 'develop': [ // New renamings go here! + { + oldName: 'Blockly.TouchGesture', + newName: 'Blockly.Gesture', + }, ] } diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 45dfe0b85..067ff4275 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -163,7 +163,7 @@ suite('Toolbox', function() { test('Toolbox clicked -> Should close flyout', function() { const hideChaffStub = sinon.stub( Blockly.WorkspaceSvg.prototype, "hideChaff"); - const evt = new MouseEvent('click', {}); + const evt = new PointerEvent('pointerdown', {}); this.toolbox.HtmlDiv.dispatchEvent(evt); sinon.assert.calledOnce(hideChaffStub); }); diff --git a/tests/mocha/tooltip_test.js b/tests/mocha/tooltip_test.js index 3d3755aa0..c5f851d7b 100644 --- a/tests/mocha/tooltip_test.js +++ b/tests/mocha/tooltip_test.js @@ -55,13 +55,9 @@ suite('Tooltip', function() { this.block.setTooltip('Test Tooltip'); // Fire pointer events directly on the relevant SVG. - // Note the 'pointerover', due to the events registered through - // Blockly.browserEvents.bind being registered as pointer events rather - // than mouse events. Mousemove event is registered directly on the - // element rather than through browserEvents. this.block.pathObject.svgPath.dispatchEvent( - new MouseEvent('pointerover')); - this.block.pathObject.svgPath.dispatchEvent(new MouseEvent('mousemove')); + new PointerEvent('pointerover')); + this.block.pathObject.svgPath.dispatchEvent(new PointerEvent('pointermove')); this.clock.runAll(); chai.assert.isTrue( diff --git a/tests/mocha/touch_test.js b/tests/mocha/touch_test.js index 77f4fb841..4afc7d802 100644 --- a/tests/mocha/touch_test.js +++ b/tests/mocha/touch_test.js @@ -19,29 +19,29 @@ }); suite('shouldHandleTouch', function() { - test('handles mousedown event', function() { - const mouseEvent = new MouseEvent('mousedown'); - chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(mouseEvent)); + test('handles pointerdown event', function() { + const pointerEvent = new PointerEvent('pointerdown'); + chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(pointerEvent)); }); - test('handles multiple mousedown events', function() { - const mouseEvent1 = new MouseEvent('mousedown'); - const mouseEvent2 = new MouseEvent('mousedown'); - Blockly.Touch.shouldHandleEvent(mouseEvent1); - chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(mouseEvent2)); + test('handles multiple pointerdown events', function() { + const pointerEvent1 = new PointerEvent('pointerdown'); + const pointerEvent2 = new PointerEvent('pointerdown'); + Blockly.Touch.shouldHandleEvent(pointerEvent1); + chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(pointerEvent2)); }); - test('does not handle mouseup if not tracking touch', function() { - const mouseEvent = new MouseEvent('mouseup'); - chai.assert.isFalse(Blockly.Touch.shouldHandleEvent(mouseEvent)); + test('does not handle pointerup if not tracking touch', function() { + const pointerEvent = new PointerEvent('pointerup'); + chai.assert.isFalse(Blockly.Touch.shouldHandleEvent(pointerEvent)); }); - test('handles mouseup if already tracking a touch', function() { - const mousedown = new MouseEvent('mousedown'); - const mouseup = new MouseEvent('mouseup'); - // Register the mousedown event first - Blockly.Touch.shouldHandleEvent(mousedown); - chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(mouseup)); + test('handles pointerup if already tracking a touch', function() { + const pointerdown = new PointerEvent('pointerdown'); + const pointerup = new PointerEvent('pointerup'); + // Register the pointerdown event first + Blockly.Touch.shouldHandleEvent(pointerdown); + chai.assert.isTrue(Blockly.Touch.shouldHandleEvent(pointerup)); }); test('handles pointerdown if this is a new touch', function() { @@ -56,13 +56,6 @@ chai.assert.isFalse(Blockly.Touch.shouldHandleEvent(pointerdown2)); }); - test('does not handle pointerdown after a mousedown', function() { - const mouseEvent = new MouseEvent('mousedown'); - const pointerdown = new PointerEvent('pointerdown', {pointerId: 1, pointerType: 'touch'}); - Blockly.Touch.shouldHandleEvent(mouseEvent); - chai.assert.isFalse(Blockly.Touch.shouldHandleEvent(pointerdown)); - }); - test('does not handle pointerup if not tracking touch', function() { const pointerup = new PointerEvent('pointerup', {pointerId: 1, pointerType: 'touch'}); chai.assert.isFalse(Blockly.Touch.shouldHandleEvent(pointerup)); @@ -77,11 +70,6 @@ }); suite('getTouchIdentifierFromEvent', function() { - test('is mouse for MouseEvents', function() { - const mousedown = new MouseEvent('mousedown'); - chai.assert.equal(Blockly.Touch.getTouchIdentifierFromEvent(mousedown), 'mouse'); - }); - test('is pointerId for mouse PointerEvents', function() { const pointerdown = new PointerEvent('pointerdown', {pointerId: 7, pointerType: 'mouse'}); chai.assert.equal(Blockly.Touch.getTouchIdentifierFromEvent(pointerdown), 7);