From 41606a6ad8cf08c80f3fd3ce3118307041eb8f7c Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 23 Nov 2022 22:11:26 +1100 Subject: [PATCH 01/14] add start of experimental new pinch touch support --- packages/core/src/types.ts | 3 +- packages/core/src/view/event/InternalEvent.ts | 137 +++++++----------- 2 files changed, 54 insertions(+), 86 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f5ff261ffa..d442fb8bc9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -266,7 +266,7 @@ export interface GraphPlugin { export type Listener = { name: string; - f: MouseEventListener | KeyboardEventListener; + f: MouseEventListener | TouchEventListener | KeyboardEventListener; }; export type ListenerTarget = { @@ -276,6 +276,7 @@ export type ListenerTarget = { export type Listenable = (EventTarget | (Window & typeof globalThis)) & ListenerTarget; export type MouseEventListener = (me: MouseEvent) => void; +export type TouchEventListener = (me: TouchEvent) => void; export type KeyboardEventListener = (ke: KeyboardEvent) => void; export type GestureEvent = Event & diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 409e684a1e..5423061ade 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -26,6 +26,7 @@ import { KeyboardEventListener, Listenable, MouseEventListener, + TouchEventListener, } from '../../types'; import { Graph } from '../Graph'; @@ -73,7 +74,7 @@ class InternalEvent { static addListener( element: Listenable, eventName: string, - funct: MouseEventListener | KeyboardEventListener + funct: MouseEventListener | TouchEventListener | KeyboardEventListener ) { element.addEventListener( eventName, @@ -95,7 +96,7 @@ class InternalEvent { static removeListener( element: Listenable, eventName: string, - funct: MouseEventListener | KeyboardEventListener + funct: MouseEventListener | TouchEventListener | KeyboardEventListener ) { element.removeEventListener(eventName, funct as EventListener, false); @@ -321,9 +322,6 @@ class InternalEvent { * function has two arguments: the mouse event and a boolean that specifies * if the wheel was moved up or down. * - * This has been tested with IE 6 and 7, Firefox (all versions), Opera and - * Safari. It does currently not work on Safari for Mac. - * * ### Example * * @example @@ -345,99 +343,68 @@ class InternalEvent { target: Listenable ) { if (funct != null) { - const wheelHandler = (evt: WheelEvent) => { - // To prevent window zoom on trackpad pinch - if (evt.ctrlKey) { - evt.preventDefault(); - } - - // Handles the event using the given function - if (Math.abs(evt.deltaX) > 0.5 || Math.abs(evt.deltaY) > 0.5) { - funct(evt, evt.deltaY == 0 ? -evt.deltaX > 0 : -evt.deltaY > 0); - } - }; + let touches: TouchList | null = null; + let startTouches: TouchList | null = null; target = target != null ? target : window; - if (Client.IS_SF && !Client.IS_TOUCH) { - let scale = 1; + const getTouchDistance = (touches: TouchList) => { + var a = touches[0].clientX - touches[1].clientX; + var b = touches[0].clientY - touches[1].clientY; + return Math.sqrt(a * a + b * b); + } - InternalEvent.addListener(target, 'gesturestart', (evt: GestureEvent) => { - InternalEvent.consume(evt); - scale = 1; + // Adds basic mouse listeners for graph event dispatching + if (Client.IS_TOUCH) { + // If a touch device, use the touch events + // TODO: Should this only happen on mobile? + // What if a user prefers using their mouse on touch-capable devices? + InternalEvent.addListener(target, 'touchstart', (evt: TouchEvent) => { + if (evt.touches && evt.touches.length > 1) { + InternalEvent.consume(evt); + startTouches = evt.touches; + } }); + InternalEvent.addListener(target, 'touchmove', (evt: TouchEvent) => { + if (!startTouches && evt.touches && evt.touches.length > 1) { + startTouches = evt.touches; + } + if (startTouches && evt.touches && evt.touches.length > 1) { + InternalEvent.consume(evt); + touches = evt.touches; - InternalEvent.addListener(target, 'gesturechange', ((evt: GestureEvent) => { - InternalEvent.consume(evt); - - if (typeof evt.scale === 'number') { - const diff = scale - evt.scale; - - if (Math.abs(diff) > 0.2) { + const diff = getTouchDistance(touches) - getTouchDistance(startTouches); + if (Math.abs(diff) > InternalEvent.PINCH_THRESHOLD) { funct(evt, diff < 0, true); - scale = evt.scale; + startTouches = evt.touches; } } - }) as EventListener); - - InternalEvent.addListener(target, 'gestureend', (evt: GestureEvent) => { + }) + InternalEvent.addListener(target, 'touchend', (evt: TouchEvent) => { InternalEvent.consume(evt); + touches = null; + startTouches = null; }); - } else { - let evtCache: EventCache = []; - let dx0 = 0; - let dy0 = 0; - - // Adds basic listeners for graph event dispatching - InternalEvent.addGestureListeners( - target, - ((evt: GestureEvent) => { - if (!isMouseEvent(evt) && evt.pointerId != null) { - evtCache.push(evt); - } - }) as EventListener, - ((evt: GestureEvent) => { - if (!isMouseEvent(evt) && evtCache.length == 2) { - // Find this event in the cache and update its record with this event - for (let i = 0; i < evtCache.length; i += 1) { - if (evt.pointerId == evtCache[i].pointerId) { - evtCache[i] = evt; - break; - } - } - - // Calculate the distance between the two pointers - const dx = Math.abs(evtCache[0].clientX - evtCache[1].clientX); - const dy = Math.abs(evtCache[0].clientY - evtCache[1].clientY); - const tx = Math.abs(dx - dx0); - const ty = Math.abs(dy - dy0); - - if ( - tx > InternalEvent.PINCH_THRESHOLD || - ty > InternalEvent.PINCH_THRESHOLD - ) { - const cx = - evtCache[0].clientX + (evtCache[1].clientX - evtCache[0].clientX) / 2; - const cy = - evtCache[0].clientY + (evtCache[1].clientY - evtCache[0].clientY) / 2; - - funct(evtCache[0], tx > ty ? dx > dx0 : dy > dy0, true, cx, cy); - - // Cache the distance for the next move event - dx0 = dx; - dy0 = dy; - } - } - }) as EventListener, - (evt) => { - evtCache = []; - dx0 = 0; - dy0 = 0; - } - ); } - InternalEvent.addListener(target, 'wheel', wheelHandler as EventListener); + // Fall back to standard mouse wheel if touch events not in progress, or not a touch device + InternalEvent.addListener(target, 'wheel', ((evt: WheelEvent) => { + if (startTouches) { + // If being handled by touch events, ignore + evt.preventDefault(); + return; + } + + // To prevent window zoom on trackpad pinch + if (evt.ctrlKey) { + evt.preventDefault(); + } + + // Handles the event using the given function + if (Math.abs(evt.deltaX) > 0.5 || Math.abs(evt.deltaY) > 0.5) { + funct(evt, evt.deltaY == 0 ? -evt.deltaX > 0 : -evt.deltaY > 0); + } + }) as EventListener); } } From 76e001850b2f354286ca0b2faa2f84f969c77e3e Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:25:40 +1100 Subject: [PATCH 02/14] start conversion of touch story --- packages/html/stories/Touch.stories.js | 435 +++++++++++++++++++++++++ packages/html/stories/stashed/Touch.js | 379 --------------------- 2 files changed, 435 insertions(+), 379 deletions(-) create mode 100644 packages/html/stories/Touch.stories.js delete mode 100644 packages/html/stories/stashed/Touch.js diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js new file mode 100644 index 0000000000..d15caf8c92 --- /dev/null +++ b/packages/html/stories/Touch.stories.js @@ -0,0 +1,435 @@ +/** + * Copyright (c) 2006-2013, JGraph Ltd + * + * Touch + * + * This example demonstrates handling of touch, + * mouse and pointer events. + */ + +import { convertPoint } from '@maxgraph/core/src/util/styleUtils'; +import { createImage } from '@maxgraph/core/src/util/domUtils'; +import { getRotatedPoint, toRadians } from '@maxgraph/core/src/util/mathUtils'; +import { getValue } from '@maxgraph/core/src/util/Utils'; +import { + ConnectionHandler, EdgeHandler, + Graph, Outline, + PanningHandler, + Point, + PopupMenuHandler, + SelectionHandler, Translations, + VertexHandler, + Client, + RubberBandHandler, + InternalEvent +} from "@maxgraph/core/src"; +import * as Constants from "@maxgraph/core/src/util/Constants"; +import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; +import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; +import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; +import {globalTypes} from "../.storybook/preview"; + +export default { + title: 'DnD_CopyPaste/Touch', + argTypes: { + ...globalTypes, + /*rubberBand: { + type: 'boolean', + defaultValue: true, + },*/ + }, +}; + +const HTML_TEMPLATE = ` + + + + + + +
+
+ +` + +const Template = ({ label, ...args }) => { + // To detect if touch events are actually supported, the following condition is recommended: + // Client.IS_TOUCH || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 + + // Disables built-in text selection and context menu while not editing text + let textEditing = (evt) => { + return graph.isEditing(); + }; + + const container = document.createElement('div'); + container.onselectstart = textEditing; + container.onmousedown = textEditing; + container.oncontextmenu = textEditing; + + // Rounded edge and vertex handles + let touchHandle = new Image('images/handle-main.png', 17, 17); + Outline.prototype.sizerImage = touchHandle; + + // Adds connect icon to selected vertex + let connectorSrc = 'images/handle-connect.png'; + + // Sets constants for touch style + // TODO: Find a means of altering these constants (ts conversion) + //Constants.HANDLE_SIZE = 16; + //Constants.LABEL_HANDLE_SIZE = 7; + + // Context menu trigger implementation depending on current selection state + // combined with support for normal popup trigger. + let cellSelected = false; + let selectionEmpty = false; + let menuShowing = false; + + // Larger tolerance and grid for real touch devices + let vertexHandlerTolerance, + edgeHandlerTolerance, + graphTolerance; + + if (Client.IS_TOUCH || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) { + //Shape.prototype.svgStrokeTolerance = 18; // TODO: Find if there's a way to replicate the previous behaviour - having this value for all shapes (ts conversion) + vertexHandlerTolerance = 12; + edgeHandlerTolerance = 12; + graphTolerance = 12; + } else { + vertexHandlerTolerance = 0; + edgeHandlerTolerance = 0; + graphTolerance = 0; + } + + class MyEdgeHandler extends EdgeHandler { + tolerance = edgeHandlerTolerance; + handleImage = touchHandle + } + + class MyPopupMenuHandler extends PopupMenuHandler { + autoExpand = true; + + isSelectOnPopup(me) { + return InternalEvent.isMouseEvent(me.getEvent()); + }; + + // Installs context menu + factoryMethod(menu, cell, evt) { + menu.addItem('Item 1', null, function () { + alert('Item 1'); + }); + menu.addSeparator(); + + var submenu1 = menu.addItem('Submenu 1', null, null); + menu.addItem('Subitem 1', null, function () { + alert('Subitem 1'); + }, submenu1); + menu.addItem('Subitem 1', null, function () { + alert('Subitem 2'); + }, submenu1); + }; + + // Shows popup menu if cell was selected or selection was empty and background was clicked + mouseUp(sender, me) { + this.popupTrigger = !graph.isEditing() && (this.popupTrigger || (!menuShowing && + !graph.isEditing() && !InternalEvent.isMouseEvent(me.getEvent()) && + ((selectionEmpty && me.getCell() == null && graph.isSelectionEmpty()) || + (cellSelected && graph.isCellSelected(me.getCell()))))); + super.apply(this, arguments); + }; + } + + class MyVertexHandler extends VertexHandler { + rotationEnabled = true; // Enables rotation handle + manageSizers = true; // Enables managing of sizers + livePreview = true; // Enables live preview + handleImage = touchHandle; + tolerance = vertexHandlerTolerance; + + init() { + // TODO: Use 4 sizers, move outside of shape + //this.singleSizer = this.state.width < 30 && this.state.height < 30; + super.apply(this, arguments); + + // Only show connector image on one cell and do not show on containers + if ( + this.graph.getPlugin('ConnectionHandler').isEnabled() && + this.state.cell.isConnectable() && + this.graph.getSelectionCount() === 1 + ) { + this.connectorImg = createImage(connectorSrc); + this.connectorImg.style.cursor = 'pointer'; + this.connectorImg.style.width = '29px'; + this.connectorImg.style.height = '29px'; + this.connectorImg.style.position = 'absolute'; + + if (!Client.IS_TOUCH) { + this.connectorImg.setAttribute('title', Translations.get('connect')); + InternalEvent.redirectMouseEvents(this.connectorImg, this.graph, this.state); + } + + // Starts connecting on touch/mouse down + InternalEvent.addGestureListeners(this.connectorImg, + ((evt) => { + this.graph.getPlugin('PopupMenuHandler').hideMenu(); + this.graph.stopEditing(false); + + let pt = convertPoint(this.graph.container, + InternalEvent.getClientX(evt), InternalEvent.getClientY(evt)); + this.graph.getPlugin('ConnectionHandler').start(this.state, pt.x, pt.y); + this.graph.isMouseDown = true; + this.graph.isMouseTrigger = InternalEvent.isMouseEvent(evt); + InternalEvent.consume(evt); + }) + ); + + this.graph.container.appendChild(this.connectorImg); + } + + this.redrawHandles(); + }; + + hideSizers() { + super.apply(this, arguments); + + if (this.connectorImg != null) { + this.connectorImg.style.visibility = 'hidden'; + } + }; + + reset() { + super.apply(this, arguments); + + if (this.connectorImg != null) { + this.connectorImg.style.visibility = ''; + } + }; + + redrawHandles() { + super.apply(this); + + if (this.state != null && this.connectorImg != null) { + let pt = new Point(); + let s = this.state; + + // Top right for single-sizer + if (this.singleSizer) { + pt.x = s.x + s.width - this.connectorImg.offsetWidth / 2; + pt.y = s.y - this.connectorImg.offsetHeight / 2; + } else { + pt.x = s.x + s.width + Constants.HANDLE_SIZE / 2 + 4 + this.connectorImg.offsetWidth / 2; + pt.y = s.y + s.height / 2; + } + + let alpha = toRadians(getValue(s.style, 'rotation', 0)); + if (alpha != 0) { + let cos = Math.cos(alpha); + let sin = Math.sin(alpha); + + let ct = new Point(s.getCenterX(), s.getCenterY()); + pt = getRotatedPoint(pt, cos, sin, ct); + } + + this.connectorImg.style.left = (pt.x - this.connectorImg.offsetWidth / 2) + 'px'; + this.connectorImg.style.top = (pt.y - this.connectorImg.offsetHeight / 2) + 'px'; + } + }; + + destroy(sender, me) { + super.apply(this, arguments); + + if (this.connectorImg != null) { + this.connectorImg.parentNode.removeChild(this.connectorImg); + this.connectorImg = null; + } + }; + } + + class MyPanningHandler extends PanningHandler { + // One finger pans (no rubberband selection) must start regardless of mouse button + isPanningTrigger(me) { + let evt = me.getEvent(); + + return (me.getState() == null && !InternalEvent.isMouseEvent(evt)) || + (InternalEvent.isPopupTrigger(evt) && (me.getState() == null || + InternalEvent.isControlDown(evt) || InternalEvent.isShiftDown(evt))); + }; + } + + class MySelectionHandler extends SelectionHandler { + // Don't clear selection if multiple cells selected + mouseDown = function (sender, me) { + super.apply(this, arguments); + + if (this.graph.isCellSelected(me.getCell()) && this.graph.getSelectionCount() > 1) { + this.delayedSelection = false; + } + }; + } + + class MyConnectionHandler extends ConnectionHandler { + createMarker() { + class MyMarker extends ConnectionHandlerCellMarker { // TODO: export this currently private class (ts conversion) + // Disable new connections via "hotspot" + isEnabled() { + return this.graph.getPlugin('ConnectionHandler').first != null; + }; + } + return new MyMarker(this.graph, this); + } + + // On connect the target is selected and we clone the cell of the preview edge for insert + selectCells(edge, target) { + if (target != null) { + this.graph.setSelectionCell(target); + } else { + this.graph.setSelectionCell(edge); + } + }; + } + + class MyCustomGraph extends Graph { + tolerance = graphTolerance; // TODO: Check this works with the mixins (ts conversion) + + createVertexHandler(state) { + return new MyVertexHandler(state); + } + + fireMouseEvent(evtName, me, sender) { + if (evtName === InternalEvent.MOUSE_DOWN) { + // For hit detection on edges + me = this.updateMouseEvent(me); + + cellSelected = this.isCellSelected(me.getCell()); + selectionEmpty = this.isSelectionEmpty(); + menuShowing = graph.getPlugin('PopupMenuHandler').isMenuShowing(); + } + this.fireMouseEvent.apply(this, arguments); + }; + + // Adds custom hit detection if native hit detection found no cell + updateMouseEvent(me) { + me = super.apply(this, arguments); + + if (me.getState() == null) { + let cell = this.getCellAt(me.graphX, me.graphY); + if (cell != null && this.isSwimlane(cell) && this.hitsSwimlaneContent(cell, me.graphX, me.graphY)) { + cell = null; + } else { + me.state = this.view.getState(cell); + + if (me.state != null && me.state.shape != null) { + this.container.style.cursor = me.state.shape.node.style.cursor; + } + } + } + + if (me.getState() == null) { + this.container.style.cursor = 'default'; + } + return me; + }; + + // Overrides double click handling to use the tolerance + dblClick(evt, cell) { + if (cell == null) { + let pt = convertPoint(this.container, + InternalEvent.getClientX(evt), InternalEvent.getClientY(evt)); + cell = this.getCellAt(pt.x, pt.y); + } + super.call(this, evt, cell); + }; + } + + // Creates the graph inside the given container + let graph = new MyCustomGraph(container, null, [ + CellEditorHandler, + TooltipHandler, + SelectionCellsHandler, + MyPopupMenuHandler, + MyConnectionHandler, + MySelectionHandler, + MyPanningHandler, + ]); + + graph.centerZoom = false; + graph.setConnectable(true); + graph.setPanning(true); + + // Creates rubberband selection + let rubberband = new RubberBandHandler(graph); + + // Tap and hold on background starts rubberband for multiple selected + // cells the cell associated with the event is deselected + graph.addListener(InternalEvent.TAP_AND_HOLD, function (sender, evt) { + if (!InternalEvent.isMultiTouchEvent(evt)) { + let me = evt.getProperty('event'); + let cell = evt.getProperty('cell'); + + if (cell == null) { + let pt = convertPoint(this.container, + InternalEvent.getClientX(me), InternalEvent.getClientY(me)); + rubberband.start(pt.x, pt.y); + } else if (graph.getSelectionCount() > 1 && graph.isCellSelected(cell)) { + graph.removeSelectionCell(cell); + } + + // Blocks further processing of the event + evt.consume(); + } + }); + + // Adds mouse wheel handling for zoom + InternalEvent.addMouseWheelListener(function (evt, up) { + if (up) { + graph.zoomIn(); + } else { + graph.zoomOut(); + } + InternalEvent.consume(evt); + }); + + graph.batchUpdate(() => { + // Get the default parent for inserting new cells. This + // is normally the first child of the root (ie. layer 0). + let parent = graph.getDefaultParent(); + + var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30); + var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30); + var e1 = graph.insertEdge(parent, null, '', v1, v2); + }); + + // Pre-fetches touch handle+connector image + new Image().src = touchHandle.src; + new Image().src = connectorSrc; + + return container; +}; + +export const Default = Template.bind({}); diff --git a/packages/html/stories/stashed/Touch.js b/packages/html/stories/stashed/Touch.js deleted file mode 100644 index a822e41a4c..0000000000 --- a/packages/html/stories/stashed/Touch.js +++ /dev/null @@ -1,379 +0,0 @@ -import { error } from '../../packages/core/src/util/gui/MaxWindow'; - -/** - * Copyright (c) 2006-2013, JGraph Ltd - * - * Touch - * - * This example demonstrates handling of touch, - * mouse and pointer events. - */ - -import React from 'react'; -import mxEvent from '../mxgraph/util/mxEvent'; -import mxGraph from '../mxgraph/view/mxGraph'; -import mxRubberband from '../mxgraph/handler/mxRubberband'; -import { convertPoint, createImage, getRotatedPoint, getValue, toRadians } from '../../packages/core/src/util/utils'; - - -const HTML_TEMPLATE = ` - - - - - - -
-
- -` - - -// To detect if touch events are actually supported, the following condition is recommended: -// Client.IS_TOUCH || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 - -// Disables built-in text selection and context menu while not editing text -let textEditing = (evt) => { - return graph.isEditing(); -}; - -container.onselectstart = textEditing; -container.onmousedown = textEditing; -container.oncontextmenu = textEditing; - -// Creates the graph inside the given container -let graph = new mxGraph(container); -graph.centerZoom = false; -graph.setConnectable(true); -graph.setPanning(true); - -// Creates rubberband selection - let rubberband = new mxRubberband(graph); - -graph.getPlugin('PopupMenuHandler').autoExpand = true; - -graph.getPlugin('PopupMenuHandler').isSelectOnPopup = function(me) { - return mxEvent.isMouseEvent(me.getEvent()); -}; - - // Installs context menu -graph.getPlugin('PopupMenuHandler').factoryMethod = function(menu, cell, evt) { - menu.addItem('Item 1', null, function() { - alert('Item 1'); - }); - menu.addSeparator(); - - var submenu1 = menu.addItem('Submenu 1', null, null); - menu.addItem('Subitem 1', null, function() { - alert('Subitem 1'); - }, submenu1); - menu.addItem('Subitem 1', null, function() { - alert('Subitem 2'); - }, submenu1); -}; - -// Context menu trigger implementation depending on current selection state -// combined with support for normal popup trigger. -let cellSelected = false; -let selectionEmpty = false; -let menuShowing = false; - -graph.fireMouseEvent = function(evtName, me, sender) { - if (evtName == mxEvent.MOUSE_DOWN) { - // For hit detection on edges - me = this.updateMouseEvent(me); - - cellSelected = this.isCellSelected(me.getCell()); - selectionEmpty = this.isSelectionEmpty(); - menuShowing = graph.getPlugin('PopupMenuHandler').isMenuShowing(); - } - mxGraph.prototype.fireMouseEvent.apply(this, arguments); -}; - -// Shows popup menu if cell was selected or selection was empty and background was clicked -graph.getPlugin('PopupMenuHandler').mouseUp = function(sender, me) { - this.popupTrigger = !graph.isEditing() && (this.popupTrigger || (!menuShowing && - !graph.isEditing() && !mxEvent.isMouseEvent(me.getEvent()) && - ((selectionEmpty && me.getCell() == null && graph.isSelectionEmpty()) || - (cellSelected && graph.isCellSelected(me.getCell()))))); - PopupMenuHandler.prototype.mouseUp.apply(this, arguments); -}; - -// Tap and hold on background starts rubberband for multiple selected -// cells the cell associated with the event is deselected -graph.addListener(mxEvent.TAP_AND_HOLD, function(sender, evt){ - if (!mxEvent.isMultiTouchEvent(evt)) { - let me = evt.getProperty('event'); - let cell = evt.getProperty('cell'); - - if (cell == null) { - let pt = convertPoint(this.container, - mxEvent.getClientX(me), mxEvent.getClientY(me)); - rubberband.start(pt.x, pt.y); - } else if (graph.getSelectionCount() > 1 && graph.isCellSelected(cell)) { - graph.removeSelectionCell(cell); - } - - // Blocks further processing of the event - evt.consume(); - } -}); - -// Adds mouse wheel handling for zoom -mxEvent.addMouseWheelListener(function(evt, up) { - if (up) { - graph.zoomIn(); - } else { - graph.zoomOut(); - } - mxEvent.consume(evt); -}); - -// Gets the default parent for inserting new cells. This -// is normally the first child of the root (ie. layer 0). -let parent = graph.getDefaultParent(); - -// Adds cells to the model in a single step -graph.getDataModel().beginUpdate(); -try { - var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30); - var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30); - var e1 = graph.insertEdge(parent, null, '', v1, v2); -} finally { - // Updates the display - graph.getDataModel().endUpdate(); -} - -// Disables new connections via "hotspot" -graph.getPlugin('ConnectionHandler').marker.isEnabled = function() { - return this.graph.getPlugin('ConnectionHandler').first != null; -}; - -// Adds custom hit detection if native hit detection found no cell -graph.updateMouseEvent = function(me) { - let me = mxGraph.prototype.updateMouseEvent.apply(this, arguments); - - if (me.getState() == null) { - let cell = this.getCellAt(me.graphX, me.graphY); - if (cell != null && this.isSwimlane(cell) && this.hitsSwimlaneContent(cell, me.graphX, me.graphY)){ - cell = null; - } else { - me.state = this.view.getState(cell); - - if (me.state != null && me.state.shape != null) { - this.container.style.cursor = me.state.shape.node.style.cursor; - } - } - } - - if (me.getState() == null) { - this.container.style.cursor = 'default'; - } - return me; -}; - -// Enables rotation handle -VertexHandler.prototype.rotationEnabled = true; - -// Enables managing of sizers -VertexHandler.prototype.manageSizers = true; - -// Enables live preview -VertexHandler.prototype.livePreview = true; - -// Sets constants for touch style -mxConstants.HANDLE_SIZE = 16; -mxConstants.LABEL_HANDLE_SIZE = 7; - -// Larger tolerance and grid for real touch devices -if (Client.IS_TOUCH || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) { - Shape.prototype.svgStrokeTolerance = 18; - VertexHandler.prototype.tolerance = 12; - mxEdgeHandler.prototype.tolerance = 12; - mxGraph.prototype.tolerance = 12; -} - -// One finger pans (no rubberband selection) must start regardless of mouse button -PanningHandler.prototype.isPanningTrigger = function(me) { - let evt = me.getEvent(); - - return (me.getState() == null && !mxEvent.isMouseEvent(evt)) || - (mxEvent.isPopupTrigger(evt) && (me.getState() == null || - mxEvent.isControlDown(evt) || mxEvent.isShiftDown(evt))); -}; - -// Don't clear selection if multiple cells selected -let graphHandlerMouseDown = SelectionHandler.prototype.mouseDown; -SelectionHandler.prototype.mouseDown = function(sender, me) { - graphHandlerMouseDown.apply(this, arguments); - - if (this.graph.isCellSelected(me.getCell()) && this.graph.getSelectionCount() > 1) { - this.delayedSelection = false; - } -}; - -// On connect the target is selected and we clone the cell of the preview edge for insert -ConnectionHandler.prototype.selectCells = function(edge, target) { - if (target != null) { - this.graph.setSelectionCell(target); - } else { - this.graph.setSelectionCell(edge); - } -}; - -// Overrides double click handling to use the tolerance -let graphDblClick = mxGraph.prototype.dblClick; -mxGraph.prototype.dblClick = function(evt, cell) { - if (cell == null) { - let pt = convertPoint(this.container, - mxEvent.getClientX(evt), mxEvent.getClientY(evt)); - cell = this.getCellAt(pt.x, pt.y); - } - graphDblClick.call(this, evt, cell); -}; - -// Rounded edge and vertex handles -let touchHandle = new Image('images/handle-main.png', 17, 17); -VertexHandler.prototype.handleImage = touchHandle; -mxEdgeHandler.prototype.handleImage = touchHandle; -Outline.prototype.sizerImage = touchHandle; - -// Pre-fetches touch handle -new Image().src = touchHandle.src; - -// Adds connect icon to selected vertex -let connectorSrc = 'images/handle-connect.png'; - -let vertexHandlerInit = VertexHandler.prototype.init; -VertexHandler.prototype.init = function() { - // TODO: Use 4 sizers, move outside of shape - //this.singleSizer = this.state.width < 30 && this.state.height < 30; - vertexHandlerInit.apply(this, arguments); - - // Only show connector image on one cell and do not show on containers - if ( - this.graph.getPlugin('ConnectionHandler').isEnabled() && - this.state.cell.isConnectable() && - this.graph.getSelectionCount() == 1 - ) { - this.connectorImg = createImage(connectorSrc); - this.connectorImg.style.cursor = 'pointer'; - this.connectorImg.style.width = '29px'; - this.connectorImg.style.height = '29px'; - this.connectorImg.style.position = 'absolute'; - - if (!Client.IS_TOUCH) { - this.connectorImg.setAttribute('title', Translations.get('connect')); - mxEvent.redirectMouseEvents(this.connectorImg, this.graph, this.state); - } - - // Starts connecting on touch/mouse down - mxEvent.addGestureListeners(this.connectorImg, - ((evt) => { - this.graph.getPlugin('PopupMenuHandler').hideMenu(); - this.graph.stopEditing(false); - - let pt = convertPoint(this.graph.container, - mxEvent.getClientX(evt), mxEvent.getClientY(evt)); - this.graph.getPlugin('ConnectionHandler').start(this.state, pt.x, pt.y); - this.graph.isMouseDown = true; - this.graph.isMouseTrigger = mxEvent.isMouseEvent(evt); - mxEvent.consume(evt); - }) - ); - - this.graph.container.appendChild(this.connectorImg); - } - - this.redrawHandles(); -}; - -let vertexHandlerHideSizers = VertexHandler.prototype.hideSizers; -VertexHandler.prototype.hideSizers = function() { - vertexHandlerHideSizers.apply(this, arguments); - - if (this.connectorImg != null) { - this.connectorImg.style.visibility = 'hidden'; - } -}; - -let vertexHandlerReset = VertexHandler.prototype.reset; -VertexHandler.prototype.reset = function() { - vertexHandlerReset.apply(this, arguments); - - if (this.connectorImg != null) { - this.connectorImg.style.visibility = ''; - } -}; - -let vertexHandlerRedrawHandles = VertexHandler.prototype.redrawHandles; -VertexHandler.prototype.redrawHandles = function() { - vertexHandlerRedrawHandles.apply(this); - - if (this.state != null && this.connectorImg != null) { - let pt = new Point(); - let s = this.state; - - // Top right for single-sizer - if (VertexHandler.prototype.singleSizer) { - pt.x = s.x + s.width - this.connectorImg.offsetWidth / 2; - pt.y = s.y - this.connectorImg.offsetHeight / 2; - } else { - pt.x = s.x + s.width + mxConstants.HANDLE_SIZE / 2 + 4 + this.connectorImg.offsetWidth / 2; - pt.y = s.y + s.height / 2; - } - - let alpha = toRadians(getValue(s.style, 'rotation', 0)); - if (alpha != 0) { - let cos = Math.cos(alpha); - let sin = Math.sin(alpha); - - let ct = new Point(s.getCenterX(), s.getCenterY()); - pt = getRotatedPoint(pt, cos, sin, ct); - } - - this.connectorImg.style.left = (pt.x - this.connectorImg.offsetWidth / 2) + 'px'; - this.connectorImg.style.top = (pt.y - this.connectorImg.offsetHeight / 2) + 'px'; - } -}; - -let vertexHandlerDestroy = VertexHandler.prototype.destroy; -VertexHandler.prototype.destroy = function(sender, me) { - vertexHandlerDestroy.apply(this, arguments); - - if (this.connectorImg != null) { - this.connectorImg.parentNode.removeChild(this.connectorImg); - this.connectorImg = null; - } -}; - -// Pre-fetches touch connector -new Image().src = connectorSrc; - From 8a3eaa839aa1b8f5e82e29095adfeebe397be7e7 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:28:44 +1100 Subject: [PATCH 03/14] move ConnectionHandlerCellMarker to external file from ConnectionHandler to allow use in touch story example --- .../src/view/handler/ConnectionHandler.ts | 112 +--------------- .../handler/ConnectionHandlerCellMarker.ts | 120 ++++++++++++++++++ packages/html/stories/Touch.stories.js | 1 + 3 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 packages/core/src/view/handler/ConnectionHandlerCellMarker.ts diff --git a/packages/core/src/view/handler/ConnectionHandler.ts b/packages/core/src/view/handler/ConnectionHandler.ts index 5afc879a25..d2c65371c1 100644 --- a/packages/core/src/view/handler/ConnectionHandler.ts +++ b/packages/core/src/view/handler/ConnectionHandler.ts @@ -58,6 +58,7 @@ import { Graph } from '../Graph'; import ConnectionConstraint from '../other/ConnectionConstraint'; import Shape from '../geometry/Shape'; import { CellStyle, ColorValue, GraphPlugin, Listenable } from '../../types'; +import ConnectionHandlerCellMarker from './ConnectionHandlerCellMarker'; type FactoryMethod = ( source: Cell | null, @@ -1986,115 +1987,4 @@ class ConnectionHandler extends EventSource implements GraphPlugin { } } -class ConnectionHandlerCellMarker extends CellMarker { - connectionHandler: ConnectionHandler; - - hotspotEnabled = true; - - constructor( - graph: Graph, - connectionHandler: ConnectionHandler, - validColor: ColorValue = DEFAULT_VALID_COLOR, - invalidColor: ColorValue = DEFAULT_INVALID_COLOR, - hotspot: number = DEFAULT_HOTSPOT - ) { - super(graph, validColor, invalidColor, hotspot); - this.connectionHandler = connectionHandler; - } - - // Overrides to return cell at location only if valid (so that - // there is no highlight for invalid cells) - getCell(me: InternalMouseEvent) { - let cell = super.getCell(me); - this.connectionHandler.error = null; - - // Checks for cell at preview point (with grid) - if (!cell && this.connectionHandler.currentPoint) { - cell = this.connectionHandler.graph.getCellAt( - this.connectionHandler.currentPoint.x, - this.connectionHandler.currentPoint.y - ); - } - - // Uses connectable parent vertex if one exists - if (cell && !cell.isConnectable() && this.connectionHandler.cell) { - const parent = this.connectionHandler.cell.getParent(); - - if (parent && parent.isVertex() && parent.isConnectable()) { - cell = parent; - } - } - - if (cell) { - if ( - (this.connectionHandler.graph.isSwimlane(cell) && - this.connectionHandler.currentPoint != null && - this.connectionHandler.graph.hitsSwimlaneContent( - cell, - this.connectionHandler.currentPoint.x, - this.connectionHandler.currentPoint.y - )) || - !this.connectionHandler.isConnectableCell(cell) - ) { - cell = null; - } - } - - if (cell) { - if (this.connectionHandler.isConnecting()) { - if (this.connectionHandler.previous) { - this.connectionHandler.error = this.connectionHandler.validateConnection( - this.connectionHandler.previous.cell, - cell - ); - - if (this.connectionHandler.error && this.connectionHandler.error.length === 0) { - cell = null; - - // Enables create target inside groups - if (this.connectionHandler.isCreateTarget(me.getEvent())) { - this.connectionHandler.error = null; - } - } - } - } else if (!this.connectionHandler.isValidSource(cell, me)) { - cell = null; - } - } else if ( - this.connectionHandler.isConnecting() && - !this.connectionHandler.isCreateTarget(me.getEvent()) && - !this.connectionHandler.graph.isAllowDanglingEdges() - ) { - this.connectionHandler.error = ''; - } - - return cell; - } - - // Sets the highlight color according to validateConnection - isValidState(state: CellState) { - if (this.connectionHandler.isConnecting()) { - return !this.connectionHandler.error; - } - return super.isValidState(state); - } - - // Overrides to use marker color only in highlight mode or for - // target selection - getMarkerColor(evt: Event, state: CellState, isValid: boolean) { - return !this.connectionHandler.connectImage || this.connectionHandler.isConnecting() - ? super.getMarkerColor(evt, state, isValid) - : NONE; - } - - // Overrides to use hotspot only for source selection otherwise - // intersects always returns true when over a cell - intersects(state: CellState, evt: InternalMouseEvent) { - if (this.connectionHandler.connectImage || this.connectionHandler.isConnecting()) { - return true; - } - return super.intersects(state, evt); - } -} - export default ConnectionHandler; diff --git a/packages/core/src/view/handler/ConnectionHandlerCellMarker.ts b/packages/core/src/view/handler/ConnectionHandlerCellMarker.ts new file mode 100644 index 0000000000..8640572c33 --- /dev/null +++ b/packages/core/src/view/handler/ConnectionHandlerCellMarker.ts @@ -0,0 +1,120 @@ +import CellMarker from "../cell/CellMarker"; +import {Graph} from "../Graph"; +import {ColorValue} from "../../types"; +import {DEFAULT_HOTSPOT, DEFAULT_INVALID_COLOR, DEFAULT_VALID_COLOR, NONE} from "../../util/Constants"; +import InternalMouseEvent from "../event/InternalMouseEvent"; +import CellState from "../cell/CellState"; +import ConnectionHandler from "./ConnectionHandler"; + +class ConnectionHandlerCellMarker extends CellMarker { + connectionHandler: ConnectionHandler; + + hotspotEnabled = true; + + constructor( + graph: Graph, + connectionHandler: ConnectionHandler, + validColor: ColorValue = DEFAULT_VALID_COLOR, + invalidColor: ColorValue = DEFAULT_INVALID_COLOR, + hotspot: number = DEFAULT_HOTSPOT + ) { + super(graph, validColor, invalidColor, hotspot); + this.connectionHandler = connectionHandler; + } + + // Overrides to return cell at location only if valid (so that + // there is no highlight for invalid cells) + getCell(me: InternalMouseEvent) { + let cell = super.getCell(me); + this.connectionHandler.error = null; + + // Checks for cell at preview point (with grid) + if (!cell && this.connectionHandler.currentPoint) { + cell = this.connectionHandler.graph.getCellAt( + this.connectionHandler.currentPoint.x, + this.connectionHandler.currentPoint.y + ); + } + + // Uses connectable parent vertex if one exists + if (cell && !cell.isConnectable() && this.connectionHandler.cell) { + const parent = this.connectionHandler.cell.getParent(); + + if (parent && parent.isVertex() && parent.isConnectable()) { + cell = parent; + } + } + + if (cell) { + if ( + (this.connectionHandler.graph.isSwimlane(cell) && + this.connectionHandler.currentPoint != null && + this.connectionHandler.graph.hitsSwimlaneContent( + cell, + this.connectionHandler.currentPoint.x, + this.connectionHandler.currentPoint.y + )) || + !this.connectionHandler.isConnectableCell(cell) + ) { + cell = null; + } + } + + if (cell) { + if (this.connectionHandler.isConnecting()) { + if (this.connectionHandler.previous) { + this.connectionHandler.error = this.connectionHandler.validateConnection( + this.connectionHandler.previous.cell, + cell + ); + + if (this.connectionHandler.error && this.connectionHandler.error.length === 0) { + cell = null; + + // Enables create target inside groups + if (this.connectionHandler.isCreateTarget(me.getEvent())) { + this.connectionHandler.error = null; + } + } + } + } else if (!this.connectionHandler.isValidSource(cell, me)) { + cell = null; + } + } else if ( + this.connectionHandler.isConnecting() && + !this.connectionHandler.isCreateTarget(me.getEvent()) && + !this.connectionHandler.graph.isAllowDanglingEdges() + ) { + this.connectionHandler.error = ''; + } + + return cell; + } + + // Sets the highlight color according to validateConnection + isValidState(state: CellState) { + if (this.connectionHandler.isConnecting()) { + return !this.connectionHandler.error; + } + return super.isValidState(state); + } + + // Overrides to use marker color only in highlight mode or for + // target selection + getMarkerColor(evt: Event, state: CellState, isValid: boolean) { + return !this.connectionHandler.connectImage || this.connectionHandler.isConnecting() + ? super.getMarkerColor(evt, state, isValid) + : NONE; + } + + // Overrides to use hotspot only for source selection otherwise + // intersects always returns true when over a cell + intersects(state: CellState, evt: InternalMouseEvent) { + if (this.connectionHandler.connectImage || this.connectionHandler.isConnecting()) { + return true; + } + return super.intersects(state, evt); + } +} + +export default ConnectionHandlerCellMarker; diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index d15caf8c92..492a27822a 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -28,6 +28,7 @@ import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; import {globalTypes} from "../.storybook/preview"; +import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; export default { title: 'DnD_CopyPaste/Touch', From fa6b76e3e8bc74b53b47a3c60bdeaf3cf74166cc Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:32:27 +1100 Subject: [PATCH 04/14] minor style updates --- packages/html/stories/Touch.stories.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 492a27822a..db98a384b2 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -7,10 +7,16 @@ * mouse and pointer events. */ -import { convertPoint } from '@maxgraph/core/src/util/styleUtils'; +import { globalTypes } from "../.storybook/preview"; +import { getValue } from '@maxgraph/core/src/util/Utils'; +import * as Constants from "@maxgraph/core/src/util/Constants"; import { createImage } from '@maxgraph/core/src/util/domUtils'; +import { convertPoint } from '@maxgraph/core/src/util/styleUtils'; +import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; import { getRotatedPoint, toRadians } from '@maxgraph/core/src/util/mathUtils'; -import { getValue } from '@maxgraph/core/src/util/Utils'; +import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; +import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; +import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; import { ConnectionHandler, EdgeHandler, Graph, Outline, @@ -19,16 +25,10 @@ import { PopupMenuHandler, SelectionHandler, Translations, VertexHandler, - Client, + Client, RubberBandHandler, InternalEvent } from "@maxgraph/core/src"; -import * as Constants from "@maxgraph/core/src/util/Constants"; -import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; -import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; -import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; -import {globalTypes} from "../.storybook/preview"; -import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; export default { title: 'DnD_CopyPaste/Touch', @@ -249,7 +249,7 @@ const Template = ({ label, ...args }) => { } let alpha = toRadians(getValue(s.style, 'rotation', 0)); - if (alpha != 0) { + if (alpha !== 0) { let cos = Math.cos(alpha); let sin = Math.sin(alpha); @@ -305,7 +305,7 @@ const Template = ({ label, ...args }) => { return new MyMarker(this.graph, this); } - // On connect the target is selected and we clone the cell of the preview edge for insert + // On connect the target is selected, and we clone the cell of the preview edge for insert selectCells(edge, target) { if (target != null) { this.graph.setSelectionCell(target); From dfdc8c459cccec77bc4490faa85db12a4dd19325 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Mon, 28 Nov 2022 22:36:41 +1100 Subject: [PATCH 05/14] update comments+style changes for touch story --- packages/html/stories/Touch.stories.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index db98a384b2..817adc7e8b 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -129,6 +129,7 @@ const Template = ({ label, ...args }) => { } class MyEdgeHandler extends EdgeHandler { + // TODO: Integrate this, potentially with the other cases in Graph.ts's createEdgeHandler (ts conversion) tolerance = edgeHandlerTolerance; handleImage = touchHandle } @@ -296,7 +297,7 @@ const Template = ({ label, ...args }) => { class MyConnectionHandler extends ConnectionHandler { createMarker() { - class MyMarker extends ConnectionHandlerCellMarker { // TODO: export this currently private class (ts conversion) + class MyMarker extends ConnectionHandlerCellMarker { // Disable new connections via "hotspot" isEnabled() { return this.graph.getPlugin('ConnectionHandler').first != null; @@ -316,7 +317,7 @@ const Template = ({ label, ...args }) => { } class MyCustomGraph extends Graph { - tolerance = graphTolerance; // TODO: Check this works with the mixins (ts conversion) + tolerance = graphTolerance; createVertexHandler(state) { return new MyVertexHandler(state); From 74f7e85f0c5d439b0f741805906b59b41fff6779 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:32:01 +1100 Subject: [PATCH 06/14] touch event story fixes --- packages/core/src/index.ts | 1 + packages/core/src/view/event/InternalEvent.ts | 4 +- packages/html/stories/Touch.stories.js | 86 ++++++++++--------- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a083d0d81f..3391270118 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,6 +78,7 @@ export { default as CellHighlight } from './view/cell/CellHighlight'; export { default as CellMarker } from './view/cell/CellMarker'; export { default as CellTracker } from './view/cell/CellTracker'; export { default as ConnectionHandler } from './view/handler/ConnectionHandler'; +export { default as ConnectionHandlerCellMarker } from './view/handler/ConnectionHandlerCellMarker'; export { default as ConstraintHandler } from './view/handler/ConstraintHandler'; export { default as EdgeHandler } from './view/handler/EdgeHandler'; export { default as EdgeSegmentHandler } from './view/handler/EdgeSegmentHandler'; diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 5423061ade..5a7bb520e7 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -357,8 +357,6 @@ class InternalEvent { // Adds basic mouse listeners for graph event dispatching if (Client.IS_TOUCH) { // If a touch device, use the touch events - // TODO: Should this only happen on mobile? - // What if a user prefers using their mouse on touch-capable devices? InternalEvent.addListener(target, 'touchstart', (evt: TouchEvent) => { if (evt.touches && evt.touches.length > 1) { InternalEvent.consume(evt); @@ -375,7 +373,7 @@ class InternalEvent { const diff = getTouchDistance(touches) - getTouchDistance(startTouches); if (Math.abs(diff) > InternalEvent.PINCH_THRESHOLD) { - funct(evt, diff < 0, true); + funct(evt, diff > 0, true); startTouches = evt.touches; } } diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 817adc7e8b..7ed74215ae 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -7,17 +7,21 @@ * mouse and pointer events. */ -import { globalTypes } from "../.storybook/preview"; -import { getValue } from '@maxgraph/core/src/util/Utils'; -import * as Constants from "@maxgraph/core/src/util/Constants"; -import { createImage } from '@maxgraph/core/src/util/domUtils'; -import { convertPoint } from '@maxgraph/core/src/util/styleUtils'; -import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; -import { getRotatedPoint, toRadians } from '@maxgraph/core/src/util/mathUtils'; -import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; -import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; -import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; +//import { getValue } from '@maxgraph/core/src/util/Utils'; +//import * as constants from "@maxgraph/core/src/util/constants"; +//import { createImage } from '@maxgraph/core/src/util/domUtils'; +//import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; +//import { getRotatedPoint, toRadians } from '@maxgraph/core/src/util/mathUtils'; +//import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; +//import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; +//import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; import { + utils, + domUtils, + styleUtils, + mathUtils, + eventUtils, + constants, ConnectionHandler, EdgeHandler, Graph, Outline, PanningHandler, @@ -27,17 +31,19 @@ import { VertexHandler, Client, RubberBandHandler, - InternalEvent -} from "@maxgraph/core/src"; + InternalEvent, + CellEditorHandler, + TooltipHandler, + SelectionCellsHandler, + ConnectionHandlerCellMarker, +} from "@maxgraph/core"; + +import { globalTypes } from "../.storybook/preview"; export default { title: 'DnD_CopyPaste/Touch', argTypes: { ...globalTypes, - /*rubberBand: { - type: 'boolean', - defaultValue: true, - },*/ }, }; @@ -103,8 +109,8 @@ const Template = ({ label, ...args }) => { // Sets constants for touch style // TODO: Find a means of altering these constants (ts conversion) - //Constants.HANDLE_SIZE = 16; - //Constants.LABEL_HANDLE_SIZE = 7; + //constants.HANDLE_SIZE = 16; + //constants.LABEL_HANDLE_SIZE = 7; // Context menu trigger implementation depending on current selection state // combined with support for normal popup trigger. @@ -138,7 +144,7 @@ const Template = ({ label, ...args }) => { autoExpand = true; isSelectOnPopup(me) { - return InternalEvent.isMouseEvent(me.getEvent()); + return eventUtils.isMouseEvent(me.getEvent()); }; // Installs context menu @@ -160,10 +166,10 @@ const Template = ({ label, ...args }) => { // Shows popup menu if cell was selected or selection was empty and background was clicked mouseUp(sender, me) { this.popupTrigger = !graph.isEditing() && (this.popupTrigger || (!menuShowing && - !graph.isEditing() && !InternalEvent.isMouseEvent(me.getEvent()) && + !graph.isEditing() && !eventUtils.isMouseEvent(me.getEvent()) && ((selectionEmpty && me.getCell() == null && graph.isSelectionEmpty()) || (cellSelected && graph.isCellSelected(me.getCell()))))); - super.apply(this, arguments); + super.mouseUp.apply(this, arguments); }; } @@ -177,7 +183,7 @@ const Template = ({ label, ...args }) => { init() { // TODO: Use 4 sizers, move outside of shape //this.singleSizer = this.state.width < 30 && this.state.height < 30; - super.apply(this, arguments); + super.init.apply(this, arguments); // Only show connector image on one cell and do not show on containers if ( @@ -185,7 +191,7 @@ const Template = ({ label, ...args }) => { this.state.cell.isConnectable() && this.graph.getSelectionCount() === 1 ) { - this.connectorImg = createImage(connectorSrc); + this.connectorImg = domUtils.createImage(connectorSrc); this.connectorImg.style.cursor = 'pointer'; this.connectorImg.style.width = '29px'; this.connectorImg.style.height = '29px'; @@ -202,11 +208,11 @@ const Template = ({ label, ...args }) => { this.graph.getPlugin('PopupMenuHandler').hideMenu(); this.graph.stopEditing(false); - let pt = convertPoint(this.graph.container, + let pt = styleUtils.convertPoint(this.graph.container, InternalEvent.getClientX(evt), InternalEvent.getClientY(evt)); this.graph.getPlugin('ConnectionHandler').start(this.state, pt.x, pt.y); this.graph.isMouseDown = true; - this.graph.isMouseTrigger = InternalEvent.isMouseEvent(evt); + this.graph.isMouseTrigger = eventUtils.isMouseEvent(evt); InternalEvent.consume(evt); }) ); @@ -218,7 +224,7 @@ const Template = ({ label, ...args }) => { }; hideSizers() { - super.apply(this, arguments); + super.hideSizers.apply(this, arguments); if (this.connectorImg != null) { this.connectorImg.style.visibility = 'hidden'; @@ -226,7 +232,7 @@ const Template = ({ label, ...args }) => { }; reset() { - super.apply(this, arguments); + super.reset.apply(this, arguments); if (this.connectorImg != null) { this.connectorImg.style.visibility = ''; @@ -234,7 +240,7 @@ const Template = ({ label, ...args }) => { }; redrawHandles() { - super.apply(this); + super.redrawHandles.apply(this); if (this.state != null && this.connectorImg != null) { let pt = new Point(); @@ -245,17 +251,17 @@ const Template = ({ label, ...args }) => { pt.x = s.x + s.width - this.connectorImg.offsetWidth / 2; pt.y = s.y - this.connectorImg.offsetHeight / 2; } else { - pt.x = s.x + s.width + Constants.HANDLE_SIZE / 2 + 4 + this.connectorImg.offsetWidth / 2; + pt.x = s.x + s.width + constants.HANDLE_SIZE / 2 + 4 + this.connectorImg.offsetWidth / 2; pt.y = s.y + s.height / 2; } - let alpha = toRadians(getValue(s.style, 'rotation', 0)); + let alpha = mathUtils.toRadians(utils.getValue(s.style, 'rotation', 0)); if (alpha !== 0) { let cos = Math.cos(alpha); let sin = Math.sin(alpha); let ct = new Point(s.getCenterX(), s.getCenterY()); - pt = getRotatedPoint(pt, cos, sin, ct); + pt = mathUtils.getRotatedPoint(pt, cos, sin, ct); } this.connectorImg.style.left = (pt.x - this.connectorImg.offsetWidth / 2) + 'px'; @@ -264,7 +270,7 @@ const Template = ({ label, ...args }) => { }; destroy(sender, me) { - super.apply(this, arguments); + super.destroy.apply(this, arguments); if (this.connectorImg != null) { this.connectorImg.parentNode.removeChild(this.connectorImg); @@ -278,7 +284,7 @@ const Template = ({ label, ...args }) => { isPanningTrigger(me) { let evt = me.getEvent(); - return (me.getState() == null && !InternalEvent.isMouseEvent(evt)) || + return (me.getState() == null && !eventUtils.isMouseEvent(evt)) || (InternalEvent.isPopupTrigger(evt) && (me.getState() == null || InternalEvent.isControlDown(evt) || InternalEvent.isShiftDown(evt))); }; @@ -286,8 +292,8 @@ const Template = ({ label, ...args }) => { class MySelectionHandler extends SelectionHandler { // Don't clear selection if multiple cells selected - mouseDown = function (sender, me) { - super.apply(this, arguments); + mouseDown(sender, me) { + super.mouseDown.apply(this, arguments); if (this.graph.isCellSelected(me.getCell()) && this.graph.getSelectionCount() > 1) { this.delayedSelection = false; @@ -332,12 +338,12 @@ const Template = ({ label, ...args }) => { selectionEmpty = this.isSelectionEmpty(); menuShowing = graph.getPlugin('PopupMenuHandler').isMenuShowing(); } - this.fireMouseEvent.apply(this, arguments); + super.fireMouseEvent.apply(this, arguments); }; // Adds custom hit detection if native hit detection found no cell updateMouseEvent(me) { - me = super.apply(this, arguments); + me = super.updateMouseEvent.apply(this, arguments); if (me.getState() == null) { let cell = this.getCellAt(me.graphX, me.graphY); @@ -361,11 +367,11 @@ const Template = ({ label, ...args }) => { // Overrides double click handling to use the tolerance dblClick(evt, cell) { if (cell == null) { - let pt = convertPoint(this.container, + let pt = styleUtils.convertPoint(this.container, InternalEvent.getClientX(evt), InternalEvent.getClientY(evt)); cell = this.getCellAt(pt.x, pt.y); } - super.call(this, evt, cell); + super.dblClick.call(this, evt, cell); }; } @@ -395,7 +401,7 @@ const Template = ({ label, ...args }) => { let cell = evt.getProperty('cell'); if (cell == null) { - let pt = convertPoint(this.container, + let pt = styleUtils.convertPoint(this.container, InternalEvent.getClientX(me), InternalEvent.getClientY(me)); rubberband.start(pt.x, pt.y); } else if (graph.getSelectionCount() > 1 && graph.isCellSelected(cell)) { From d7f2a7fa343180d04340d0e02476eac67b51c215 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:34:16 +1100 Subject: [PATCH 07/14] removed unneeded imports from touch event story --- packages/html/stories/Touch.stories.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 7ed74215ae..96157db79e 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -7,14 +7,6 @@ * mouse and pointer events. */ -//import { getValue } from '@maxgraph/core/src/util/Utils'; -//import * as constants from "@maxgraph/core/src/util/constants"; -//import { createImage } from '@maxgraph/core/src/util/domUtils'; -//import TooltipHandler from "@maxgraph/core/src/view/handler/TooltipHandler"; -//import { getRotatedPoint, toRadians } from '@maxgraph/core/src/util/mathUtils'; -//import CellEditorHandler from "@maxgraph/core/src/view/handler/CellEditorHandler"; -//import SelectionCellsHandler from "@maxgraph/core/src/view/handler/SelectionCellsHandler"; -//import ConnectionHandlerCellMarker from "@maxgraph/core/src/view/handler/ConnectionHandlerCellMarker"; import { utils, domUtils, From a75156de39043b0654d3e6a1f118a4f189e60ca0 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:48:22 +1100 Subject: [PATCH 08/14] add ability to use capture events to InternalEvent's addListener and made it so mousemove/pointermove events are ignored when there's multitouch and addMouseWheelListener enabled --- packages/core/src/view/event/InternalEvent.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 5a7bb520e7..b2b8260fdb 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -74,12 +74,13 @@ class InternalEvent { static addListener( element: Listenable, eventName: string, - funct: MouseEventListener | TouchEventListener | KeyboardEventListener + funct: MouseEventListener | TouchEventListener | KeyboardEventListener, + capture: boolean = false, ) { element.addEventListener( eventName, funct as EventListener, - supportsPassive ? { passive: false } : false + supportsPassive ? { passive: false, capture: capture } : capture ); if (!element.mxListenerList) { @@ -362,7 +363,7 @@ class InternalEvent { InternalEvent.consume(evt); startTouches = evt.touches; } - }); + }, true); InternalEvent.addListener(target, 'touchmove', (evt: TouchEvent) => { if (!startTouches && evt.touches && evt.touches.length > 1) { startTouches = evt.touches; @@ -377,12 +378,23 @@ class InternalEvent { startTouches = evt.touches; } } - }) + }, true) InternalEvent.addListener(target, 'touchend', (evt: TouchEvent) => { InternalEvent.consume(evt); touches = null; startTouches = null; - }); + }, true); + + InternalEvent.addListener(target, 'mousemove', (evt: TouchEvent) => { + if (startTouches) { + InternalEvent.consume(evt); + } + }, true); + InternalEvent.addListener(target, 'pointermove', (evt: TouchEvent) => { + if (startTouches) { + InternalEvent.consume(evt); + } + }, true); } // Fall back to standard mouse wheel if touch events not in progress, or not a touch device From e8f85e17dcac562df863ccd5bd9680bada7f3ad6 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 21:24:40 +1100 Subject: [PATCH 09/14] add styles to touch story container --- packages/html/stories/Touch.stories.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 96157db79e..4568adfaa0 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -39,7 +39,8 @@ export default { }, }; -const HTML_TEMPLATE = ` +// TODO: Centralize to global MaxGraph CSS or assign these styles explicitly +const MENU_STYLES = ` - - - - - -
-
- ` const Template = ({ label, ...args }) => { @@ -92,6 +84,13 @@ const Template = ({ label, ...args }) => { container.onmousedown = textEditing; container.oncontextmenu = textEditing; + container.style.position = 'relative'; + container.style.overflow = 'hidden'; + container.style.width = `${args.width}px`; + container.style.height = `${args.height}px`; + container.style.background = 'url(/images/grid.gif)'; + container.style.cursor = 'default'; + // Rounded edge and vertex handles let touchHandle = new Image('images/handle-main.png', 17, 17); Outline.prototype.sizerImage = touchHandle; From 1b14f1d87abf37d5dac30694b5f7b002e2465765 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 21:32:46 +1100 Subject: [PATCH 10/14] replace remaining var's with let --- packages/html/stories/Touch.stories.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 4568adfaa0..4760696743 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -145,7 +145,7 @@ const Template = ({ label, ...args }) => { }); menu.addSeparator(); - var submenu1 = menu.addItem('Submenu 1', null, null); + let submenu1 = menu.addItem('Submenu 1', null, null); menu.addItem('Subitem 1', null, function () { alert('Subitem 1'); }, submenu1); @@ -419,9 +419,9 @@ const Template = ({ label, ...args }) => { // is normally the first child of the root (ie. layer 0). let parent = graph.getDefaultParent(); - var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30); - var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30); - var e1 = graph.insertEdge(parent, null, '', v1, v2); + let v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30); + let v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30); + let e1 = graph.insertEdge(parent, null, '', v1, v2); }); // Pre-fetches touch handle+connector image From ee48906eedbdba52c35cf13ad85ecf2140539492 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 21:34:46 +1100 Subject: [PATCH 11/14] replace remaining var's with let --- packages/core/src/view/event/InternalEvent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index b2b8260fdb..1809aeaaa3 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -350,8 +350,8 @@ class InternalEvent { target = target != null ? target : window; const getTouchDistance = (touches: TouchList) => { - var a = touches[0].clientX - touches[1].clientX; - var b = touches[0].clientY - touches[1].clientY; + let a = touches[0].clientX - touches[1].clientX; + let b = touches[0].clientY - touches[1].clientY; return Math.sqrt(a * a + b * b); } From b957841744aaafd70a621d7cb16d1904f9a80867 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Wed, 30 Nov 2022 21:38:44 +1100 Subject: [PATCH 12/14] fix eslint errors --- packages/core/src/view/event/InternalEvent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 1809aeaaa3..9ed52c00a0 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -75,7 +75,7 @@ class InternalEvent { element: Listenable, eventName: string, funct: MouseEventListener | TouchEventListener | KeyboardEventListener, - capture: boolean = false, + capture = false, ) { element.addEventListener( eventName, @@ -350,8 +350,8 @@ class InternalEvent { target = target != null ? target : window; const getTouchDistance = (touches: TouchList) => { - let a = touches[0].clientX - touches[1].clientX; - let b = touches[0].clientY - touches[1].clientY; + const a = touches[0].clientX - touches[1].clientX; + const b = touches[0].clientY - touches[1].clientY; return Math.sqrt(a * a + b * b); } From ddec85adf86c0b09e7b6827d66975b46d308505c Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:58:45 +1100 Subject: [PATCH 13/14] remove support for safari-specific gestures api and fixed touch events for iOS/safari --- packages/core/src/view/GraphView.ts | 21 +--------- packages/core/src/view/event/InternalEvent.ts | 35 +++++++++++------ .../core/src/view/handler/PanningHandler.ts | 25 ------------ .../core/src/view/handler/PopupMenuHandler.ts | 9 ----- .../src/view/handler/RubberBandHandler.ts | 10 ----- packages/core/src/view/mixins/EventsMixin.ts | 39 ------------------- packages/html/stories/Touch.stories.js | 15 ++++--- 7 files changed, 35 insertions(+), 119 deletions(-) diff --git a/packages/core/src/view/GraphView.ts b/packages/core/src/view/GraphView.ts index dfa043dc7e..435ec0b60c 100644 --- a/packages/core/src/view/GraphView.ts +++ b/packages/core/src/view/GraphView.ts @@ -2071,25 +2071,6 @@ export class GraphView extends EventSource { const graph = this.graph; const { container } = graph; - // Support for touch device gestures (eg. pinch to zoom) - // Double-tap handling is implemented in mxGraph.fireMouseEvent - if (Client.IS_TOUCH) { - InternalEvent.addListener(container, 'gesturestart', ((evt: MouseEvent) => { - graph.fireGestureEvent(evt); - InternalEvent.consume(evt); - }) as EventListener); - - InternalEvent.addListener(container, 'gesturechange', ((evt: MouseEvent) => { - graph.fireGestureEvent(evt); - InternalEvent.consume(evt); - }) as EventListener); - - InternalEvent.addListener(container, 'gestureend', ((evt: MouseEvent) => { - graph.fireGestureEvent(evt); - InternalEvent.consume(evt); - }) as EventListener); - } - // Fires event only for one pointer per gesture let pointerId: number | null = null; @@ -2100,7 +2081,7 @@ export class GraphView extends EventSource { // Condition to avoid scrollbar events starting a rubberband selection if ( this.isContainerEvent(evt) && - ((!Client.IS_GC && !Client.IS_SF) || !this.isScrollEvent(evt)) + (!Client.IS_GC && !Client.IS_SF) ) { graph.fireMouseEvent(InternalEvent.MOUSE_DOWN, new InternalMouseEvent(evt)); // @ts-ignore diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 9ed52c00a0..919dcac87c 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -341,51 +341,64 @@ class InternalEvent { */ static addMouseWheelListener( funct: (event: Event, up: boolean, force?: boolean, cx?: number, cy?: number) => void, - target: Listenable + target?: Listenable ) { if (funct != null) { - let touches: TouchList | null = null; - let startTouches: TouchList | null = null; + type TouchArray = { clientX: number, clientY: number }[]; + let touches: TouchArray | null = null; + let startTouches: TouchArray | null = null; target = target != null ? target : window; - const getTouchDistance = (touches: TouchList) => { + const getTouchDistance = (touches: TouchArray) => { const a = touches[0].clientX - touches[1].clientX; const b = touches[0].clientY - touches[1].clientY; return Math.sqrt(a * a + b * b); } + const touchesToArray = (touches: TouchList): TouchArray => { + // Safari seems to use the same TouchList object unless + // the values are copied, so copy+output the values we need + const out = []; + for (let i=0; i { if (evt.touches && evt.touches.length > 1) { InternalEvent.consume(evt); - startTouches = evt.touches; + startTouches = touchesToArray(evt.touches); } }, true); InternalEvent.addListener(target, 'touchmove', (evt: TouchEvent) => { if (!startTouches && evt.touches && evt.touches.length > 1) { - startTouches = evt.touches; + startTouches = touchesToArray(evt.touches); } if (startTouches && evt.touches && evt.touches.length > 1) { InternalEvent.consume(evt); - touches = evt.touches; + touches = touchesToArray(evt.touches); const diff = getTouchDistance(touches) - getTouchDistance(startTouches); if (Math.abs(diff) > InternalEvent.PINCH_THRESHOLD) { funct(evt, diff > 0, true); - startTouches = evt.touches; + startTouches = touchesToArray(evt.touches); } } }, true) InternalEvent.addListener(target, 'touchend', (evt: TouchEvent) => { - InternalEvent.consume(evt); + if (startTouches) { + InternalEvent.consume(evt); + } touches = null; startTouches = null; }, true); - InternalEvent.addListener(target, 'mousemove', (evt: TouchEvent) => { + /*InternalEvent.addListener(target, 'mousemove', (evt: TouchEvent) => { if (startTouches) { InternalEvent.consume(evt); } @@ -394,7 +407,7 @@ class InternalEvent { if (startTouches) { InternalEvent.consume(evt); } - }, true); + }, true);*/ } // Fall back to standard mouse wheel if touch events not in progress, or not a touch device diff --git a/packages/core/src/view/handler/PanningHandler.ts b/packages/core/src/view/handler/PanningHandler.ts index dd90ff5129..ae7483eae7 100644 --- a/packages/core/src/view/handler/PanningHandler.ts +++ b/packages/core/src/view/handler/PanningHandler.ts @@ -85,31 +85,6 @@ class PanningHandler extends EventSource implements GraphPlugin { this.graph.addListener(InternalEvent.FIRE_MOUSE_EVENT, this.forcePanningHandler); - // Handles pinch gestures - this.gestureHandler = (sender: EventSource, eo: EventObject) => { - if (this.isPinchEnabled()) { - const evt = eo.getProperty('event'); - - if (!isConsumed(evt) && evt.type === 'gesturestart') { - this.initialScale = this.graph.view.scale; - - // Forces start of panning when pinch gesture starts - if (!this.active && this.mouseDownEvent) { - this.start(this.mouseDownEvent); - this.mouseDownEvent = null; - } - } else if (evt.type === 'gestureend' && this.initialScale !== 0) { - this.initialScale = 0; - } - - if (this.initialScale !== 0) { - this.zoomGraph(evt); - } - } - }; - - this.graph.addListener(InternalEvent.GESTURE, this.gestureHandler); - this.mouseUpListener = () => { if (this.active) { this.reset(); diff --git a/packages/core/src/view/handler/PopupMenuHandler.ts b/packages/core/src/view/handler/PopupMenuHandler.ts index 4cbc065240..8b72d13ad3 100644 --- a/packages/core/src/view/handler/PopupMenuHandler.ts +++ b/packages/core/src/view/handler/PopupMenuHandler.ts @@ -43,18 +43,9 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin { this.graph = graph; this.graph.addMouseListener(this); - // Does not show menu if any touch gestures take place after the trigger - this.gestureHandler = (sender: EventSource, eo: EventObject) => { - this.inTolerance = false; - }; - - this.graph.addListener(InternalEvent.GESTURE, this.gestureHandler); - this.init(); } - gestureHandler: (sender: EventSource, eo: EventObject) => void; - inTolerance = false; popupTrigger = false; diff --git a/packages/core/src/view/handler/RubberBandHandler.ts b/packages/core/src/view/handler/RubberBandHandler.ts index bf7c5dcafa..2565a0daee 100644 --- a/packages/core/src/view/handler/RubberBandHandler.ts +++ b/packages/core/src/view/handler/RubberBandHandler.ts @@ -70,20 +70,10 @@ class RubberBandHandler implements GraphPlugin { }; this.graph.addListener(InternalEvent.PAN, this.panHandler); - - // Does not show menu if any touch gestures take place after the trigger - this.gestureHandler = (sender: EventSource, eo: EventObject) => { - if (this.first) { - this.reset(); - } - }; - - this.graph.addListener(InternalEvent.GESTURE, this.gestureHandler); } forceRubberbandHandler: Function; panHandler: Function; - gestureHandler: Function; graph: Graph; first: Point | null = null; destroyed = false; diff --git a/packages/core/src/view/mixins/EventsMixin.ts b/packages/core/src/view/mixins/EventsMixin.ts index 2939203ff8..ea91674872 100644 --- a/packages/core/src/view/mixins/EventsMixin.ts +++ b/packages/core/src/view/mixins/EventsMixin.ts @@ -115,7 +115,6 @@ declare module '../Graph' { me: InternalMouseEvent, sender: EventSource ) => void; - fireGestureEvent: (evt: MouseEvent, cell?: Cell | null) => void; sizeDidChange: () => void; isCloneEvent: (evt: MouseEvent) => boolean; isTransparentClickEvent: (evt: MouseEvent) => boolean; @@ -230,7 +229,6 @@ type PartialEvents = Pick< | 'getEventState' | 'fireMouseEvent' | 'consumeMouseEvent' - | 'fireGestureEvent' | 'sizeDidChange' | 'isCloneEvent' | 'isTransparentClickEvent' @@ -1084,43 +1082,6 @@ const EventsMixin: PartialType = { } }, - /** - * Dispatches a {@link InternalEvent.GESTURE} event. The following example will resize the - * cell under the mouse based on the scale property of the native touch event. - * - * ```javascript - * graph.addListener(mxEvent.GESTURE, function(sender, eo) - * { - * var evt = eo.getProperty('event'); - * var state = graph.view.getState(eo.getProperty('cell')); - * - * if (graph.isEnabled() && graph.isCellResizable(state.cell) && Math.abs(1 - evt.scale) > 0.2) - * { - * var scale = graph.view.scale; - * var tr = graph.view.translate; - * - * var w = state.width * evt.scale; - * var h = state.height * evt.scale; - * var x = state.x - (w - state.width) / 2; - * var y = state.y - (h - state.height) / 2; - * - * var bounds = new mxRectangle(graph.snap(x / scale) - tr.x, - * graph.snap(y / scale) - tr.y, graph.snap(w / scale), graph.snap(h / scale)); - * graph.resizeCell(state.cell, bounds); - * eo.consume(); - * } - * }); - * ``` - * - * @param evt Gestureend event that represents the gesture. - * @param cell Optional {@link Cell} associated with the gesture. - */ - fireGestureEvent(evt, cell = null) { - // Resets double tap event handling when gestures take place - this.lastTouchTime = 0; - this.fireEvent(new EventObject(InternalEvent.GESTURE, { event: evt, cell })); - }, - /** * Called when the size of the graph has changed. This implementation fires * a {@link size} event after updating the clipping region of the SVG element in diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index 4760696743..fb5d5a34f7 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -167,7 +167,8 @@ const Template = ({ label, ...args }) => { class MyVertexHandler extends VertexHandler { rotationEnabled = true; // Enables rotation handle manageSizers = true; // Enables managing of sizers - livePreview = true; // Enables live preview + // TODO: It appears live preview is broken on Safari/iOS (iPhone) when resizing nodes! + //livePreview = true; // Enables live preview handleImage = touchHandle; tolerance = vertexHandlerTolerance; @@ -276,8 +277,8 @@ const Template = ({ label, ...args }) => { let evt = me.getEvent(); return (me.getState() == null && !eventUtils.isMouseEvent(evt)) || - (InternalEvent.isPopupTrigger(evt) && (me.getState() == null || - InternalEvent.isControlDown(evt) || InternalEvent.isShiftDown(evt))); + (eventUtils.isPopupTrigger(evt) && (me.getState() == null || + eventUtils.isControlDown(evt) || eventUtils.isShiftDown(evt))); }; } @@ -387,13 +388,13 @@ const Template = ({ label, ...args }) => { // Tap and hold on background starts rubberband for multiple selected // cells the cell associated with the event is deselected graph.addListener(InternalEvent.TAP_AND_HOLD, function (sender, evt) { - if (!InternalEvent.isMultiTouchEvent(evt)) { + if (!eventUtils.isMultiTouchEvent(evt)) { let me = evt.getProperty('event'); let cell = evt.getProperty('cell'); if (cell == null) { let pt = styleUtils.convertPoint(this.container, - InternalEvent.getClientX(me), InternalEvent.getClientY(me)); + eventUtils.getClientX(me), eventUtils.getClientY(me)); rubberband.start(pt.x, pt.y); } else if (graph.getSelectionCount() > 1 && graph.isCellSelected(cell)) { graph.removeSelectionCell(cell); @@ -431,4 +432,8 @@ const Template = ({ label, ...args }) => { return container; }; +window.onerror = function(a, b, c, d, e) { + alert(a+'\n'+b+'\n'+c+'\n'+d+'\n'+e+'\n'+e.stack) +} + export const Default = Template.bind({}); From c15512792eb5e0fa5d9f35c969c18ff7b00fbbc7 Mon Sep 17 00:00:00 2001 From: Dave Morrissey <20507948+mcyph@users.noreply.github.com> Date: Sat, 3 Dec 2022 15:25:59 +1100 Subject: [PATCH 14/14] remove references to gestureHandler and eslint fixes --- packages/core/src/view/event/InternalEvent.ts | 17 ++++++++--------- .../core/src/view/handler/PanningHandler.ts | 4 +--- .../core/src/view/handler/PopupMenuHandler.ts | 1 - packages/html/stories/Touch.stories.js | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/core/src/view/event/InternalEvent.ts b/packages/core/src/view/event/InternalEvent.ts index 919dcac87c..7a19b2bf76 100644 --- a/packages/core/src/view/event/InternalEvent.ts +++ b/packages/core/src/view/event/InternalEvent.ts @@ -18,11 +18,9 @@ limitations under the License. import InternalMouseEvent from './InternalMouseEvent'; import Client from '../../Client'; -import { isConsumed, isMouseEvent } from '../../util/EventUtils'; +import { isConsumed } from '../../util/EventUtils'; import CellState from '../cell/CellState'; import { - EventCache, - GestureEvent, KeyboardEventListener, Listenable, MouseEventListener, @@ -34,6 +32,7 @@ import { Graph } from '../Graph'; // see https://github.com/Modernizr/Modernizr/issues/1894 let supportsPassive = false; + try { document.addEventListener( 'test', @@ -55,7 +54,7 @@ try { * @class InternalEvent * * Cross-browser DOM event support. For internal event handling, - * {@link mxEventSource} and the graph event dispatch loop in {@link graph} are used. + * {@link EventSource} and the graph event dispatch loop in {@link graph} are used. * * ### Memory Leaks: * @@ -398,7 +397,7 @@ class InternalEvent { startTouches = null; }, true); - /*InternalEvent.addListener(target, 'mousemove', (evt: TouchEvent) => { + InternalEvent.addListener(target, 'mousemove', (evt: TouchEvent) => { if (startTouches) { InternalEvent.consume(evt); } @@ -407,27 +406,27 @@ class InternalEvent { if (startTouches) { InternalEvent.consume(evt); } - }, true);*/ + }, true); } // Fall back to standard mouse wheel if touch events not in progress, or not a touch device InternalEvent.addListener(target, 'wheel', ((evt: WheelEvent) => { if (startTouches) { // If being handled by touch events, ignore - evt.preventDefault(); + InternalEvent.consume(evt); return; } // To prevent window zoom on trackpad pinch if (evt.ctrlKey) { - evt.preventDefault(); + InternalEvent.consume(evt); } // Handles the event using the given function if (Math.abs(evt.deltaX) > 0.5 || Math.abs(evt.deltaY) > 0.5) { funct(evt, evt.deltaY == 0 ? -evt.deltaX > 0 : -evt.deltaY > 0); } - }) as EventListener); + }) as EventListener, true); } } diff --git a/packages/core/src/view/handler/PanningHandler.ts b/packages/core/src/view/handler/PanningHandler.ts index ae7483eae7..8223ae385c 100644 --- a/packages/core/src/view/handler/PanningHandler.ts +++ b/packages/core/src/view/handler/PanningHandler.ts @@ -108,7 +108,7 @@ class PanningHandler extends EventSource implements GraphPlugin { /** * Specifies if panning should be active for the left mouse button. - * Setting this to true may conflict with {@link Rubberband}. Default is false. + * Setting this to true may conflict with {@link RubberbandHandler}. Default is false. */ useLeftButtonForPanning = false; @@ -184,7 +184,6 @@ class PanningHandler extends EventSource implements GraphPlugin { active = false; forcePanningHandler: (sender: EventSource, evt: EventObject) => void; - gestureHandler: (sender: EventSource, evt: EventObject) => void; mouseUpListener: MouseEventListener; @@ -418,7 +417,6 @@ class PanningHandler extends EventSource implements GraphPlugin { onDestroy() { this.graph.removeMouseListener(this); this.graph.removeListener(this.forcePanningHandler); - this.graph.removeListener(this.gestureHandler); InternalEvent.removeListener(document, 'mouseup', this.mouseUpListener); } } diff --git a/packages/core/src/view/handler/PopupMenuHandler.ts b/packages/core/src/view/handler/PopupMenuHandler.ts index 8b72d13ad3..99df389c25 100644 --- a/packages/core/src/view/handler/PopupMenuHandler.ts +++ b/packages/core/src/view/handler/PopupMenuHandler.ts @@ -192,7 +192,6 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin { */ onDestroy() { this.graph.removeMouseListener(this); - this.graph.removeListener(this.gestureHandler); // Supercall super.destroy(); diff --git a/packages/html/stories/Touch.stories.js b/packages/html/stories/Touch.stories.js index fb5d5a34f7..da87c404f5 100644 --- a/packages/html/stories/Touch.stories.js +++ b/packages/html/stories/Touch.stories.js @@ -168,7 +168,7 @@ const Template = ({ label, ...args }) => { rotationEnabled = true; // Enables rotation handle manageSizers = true; // Enables managing of sizers // TODO: It appears live preview is broken on Safari/iOS (iPhone) when resizing nodes! - //livePreview = true; // Enables live preview + livePreview = true; // Enables live preview handleImage = touchHandle; tolerance = vertexHandlerTolerance; @@ -413,7 +413,7 @@ const Template = ({ label, ...args }) => { graph.zoomOut(); } InternalEvent.consume(evt); - }); + }, container); graph.batchUpdate(() => { // Get the default parent for inserting new cells. This