Improve support for touch-devices #1070

This commit is contained in:
Jan Oberhauser 2020-10-23 13:44:34 +02:00
parent ada485ed5c
commit 4df74771e3
8 changed files with 113 additions and 60 deletions

View file

@ -25,7 +25,8 @@
"test:unit": "vue-cli-service test:unit" "test:unit": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {
"uuid": "^8.1.0" "uuid": "^8.1.0",
"vue2-touch-events": "^2.3.2"
}, },
"devDependencies": { "devDependencies": {
"@beyonk/google-fonts-webpack-plugin": "^1.2.3", "@beyonk/google-fonts-webpack-plugin": "^1.2.3",

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="node-wrapper" :style="nodePosition"> <div class="node-wrapper" :style="nodePosition">
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick"> <div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:end="mouseLeftClick">
<div v-if="hasIssues" class="node-info-icon node-issues"> <div v-if="hasIssues" class="node-info-icon node-issues">
<el-tooltip placement="top" effect="light"> <el-tooltip placement="top" effect="light">
<div slot="content" v-html="nodeIssues"></div> <div slot="content" v-html="nodeIssues"></div>
@ -13,19 +13,19 @@
<font-awesome-icon icon="sync-alt" spin /> <font-awesome-icon icon="sync-alt" spin />
</div> </div>
<div class="node-options" v-if="!isReadOnly"> <div class="node-options" v-if="!isReadOnly">
<div @click.stop.left="deleteNode" class="option" title="Delete Node" > <div v-touch:tap="deleteNode" class="option" title="Delete Node" >
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
</div> </div>
<div @click.stop.left="disableNode" class="option" title="Activate/Deactivate Node" > <div v-touch:tap="disableNode" v-touch-options="{disableClick: true}" class="option" title="Activate/Deactivate Node" >
<font-awesome-icon :icon="nodeDisabledIcon" /> <font-awesome-icon :icon="nodeDisabledIcon" />
</div> </div>
<div @click.stop.left="duplicateNode" class="option" title="Duplicate Node" > <div v-touch:tap="duplicateNode" class="option" title="Duplicate Node" >
<font-awesome-icon icon="clone" /> <font-awesome-icon icon="clone" />
</div> </div>
<div @click.stop.left="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly"> <div v-touch:tap="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
<font-awesome-icon class="execute-icon" icon="cog" /> <font-awesome-icon class="execute-icon" icon="cog" />
</div> </div>
<div @click.stop.left="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning"> <div v-touch:tap="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
<font-awesome-icon class="execute-icon" icon="play-circle" /> <font-awesome-icon class="execute-icon" icon="play-circle" />
</div> </div>
</div> </div>

View file

@ -0,0 +1,30 @@
import Vue from 'vue';
export const deviceSupportHelpers = Vue.extend({
data() {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isMacOs: /(ipad|iphone|ipod|mac)/i.test(navigator.platform),
};
},
computed: {
// TODO: Check if used anywhere
controlKeyCode(): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
},
methods: {
isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
},
},
});

View file

@ -2,20 +2,19 @@ import { INodeUi } from '@/Interface';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
export const mouseSelect = mixins(nodeIndex).extend({ export const mouseSelect = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () { data () {
return { return {
selectActive: false, selectActive: false,
selectBox: document.createElement('span'), selectBox: document.createElement('span'),
}; };
}, },
computed: {
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
mounted () { mounted () {
this.createSelectBox(); this.createSelectBox();
}, },
@ -34,6 +33,9 @@ export const mouseSelect = mixins(nodeIndex).extend({
this.$el.appendChild(this.selectBox); this.$el.appendChild(this.selectBox);
}, },
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) { if (this.isMacOs) {
return e.metaKey; return e.metaKey;
} }
@ -125,6 +127,13 @@ export const mouseSelect = mixins(nodeIndex).extend({
}, },
mouseUpMouseSelect (e: MouseEvent) { mouseUpMouseSelect (e: MouseEvent) {
if (this.selectActive === false) { if (this.selectActive === false) {
if (this.isTouchDevice === true) {
// @ts-ignore
if (e.target && e.target.id.includes('node-view')) {
// Deselect all nodes
this.deselectAllNodes();
}
}
// If it is not active return direcly. // If it is not active return direcly.
// Else normal node dragging will not work. // Else normal node dragging will not work.
return; return;

View file

@ -1,41 +1,42 @@
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
export const moveNodeWorkflow = mixins(nodeIndex).extend({ export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () { data () {
return { return {
moveLastPosition: [0, 0], moveLastPosition: [0, 0],
}; };
}, },
computed: {
controlKeyCode (): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
methods: { methods: {
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { getMousePosition(e: MouseEvent | TouchEvent) {
if (this.isMacOs) { // @ts-ignore
return e.metaKey; const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
} // @ts-ignore
return e.ctrlKey; const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
return {
x,
y,
};
}, },
moveWorkflow (e: MouseEvent) { moveWorkflow (e: MouseEvent) {
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition; const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]); const position = this.getMousePosition(e);
const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]);
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]); this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]);
// Update the last position // Update the last position
this.moveLastPosition[0] = e.pageX; this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = e.pageY; this.moveLastPosition[1] = position.y;
}, },
mouseDownMoveWorkflow (e: MouseEvent) { mouseDownMoveWorkflow (e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) { if (this.isCtrlKeyPressed(e) === false) {
@ -51,8 +52,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.$store.commit('setNodeViewMoveInProgress', true); this.$store.commit('setNodeViewMoveInProgress', true);
this.moveLastPosition[0] = e.pageX; const position = this.getMousePosition(e);
this.moveLastPosition[1] = e.pageY;
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
// @ts-ignore // @ts-ignore
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow); this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
@ -72,6 +75,15 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
// Nothing else to do. Simply leave the node view at the current offset // Nothing else to do. Simply leave the node view at the current offset
}, },
mouseMoveNodeWorkflow (e: MouseEvent) { mouseMoveNodeWorkflow (e: MouseEvent) {
// @ts-ignore
if (e.target && !e.target.id.includes('node-view')) {
return;
}
if (this.$store.getters.isActionActive('dragActive')) {
return;
}
if (e.buttons === 0) { if (e.buttons === 0) {
// Mouse button is not pressed anymore so stop selection mode // Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then // Happens normally when mouse leave the view pressed and then

View file

@ -2,20 +2,20 @@ import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interfac
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex'; import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX } from '@/constants'; import { NODE_NAME_PREFIX } from '@/constants';
export const nodeBase = mixins(nodeIndex).extend({ export const nodeBase = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
mounted () { mounted () {
// Initialize the node // Initialize the node
if (this.data !== null) { if (this.data !== null) {
this.__addNode(this.data); this.__addNode(this.data);
} }
}, },
data () {
return {
};
},
computed: { computed: {
data (): INodeUi { data (): INodeUi {
return this.$store.getters.nodeByName(this.name); return this.$store.getters.nodeByName(this.name);
@ -26,9 +26,6 @@ export const nodeBase = mixins(nodeIndex).extend({
} }
return false; return false;
}, },
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
nodeName (): string { nodeName (): string {
return NODE_NAME_PREFIX + this.nodeIndex; return NODE_NAME_PREFIX + this.nodeIndex;
}, },
@ -337,13 +334,6 @@ export const nodeBase = mixins(nodeIndex).extend({
}, },
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
},
mouseLeftClick (e: MouseEvent) { mouseLeftClick (e: MouseEvent) {
if (this.$store.getters.isActionActive('dragActive')) { if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive'); this.$store.commit('removeActiveAction', 'dragActive');

View file

@ -5,6 +5,7 @@ import Vue from 'vue';
import 'prismjs'; import 'prismjs';
import 'prismjs/themes/prism.css'; import 'prismjs/themes/prism.css';
import 'vue-prism-editor/dist/VuePrismEditor.css'; import 'vue-prism-editor/dist/VuePrismEditor.css';
import Vue2TouchEvents from 'vue2-touch-events';
import * as ElementUI from 'element-ui'; import * as ElementUI from 'element-ui';
// @ts-ignore // @ts-ignore
@ -91,6 +92,9 @@ import {
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { store } from './store'; import { store } from './store';
Vue.use(Vue2TouchEvents);
Vue.use(ElementUI, { locale }); Vue.use(ElementUI, { locale });
library.add(faAngleDoubleLeft); library.add(faAngleDoubleLeft);

View file

@ -3,11 +3,15 @@
<div <div
class="node-view-wrapper" class="node-view-wrapper"
:class="workflowClasses" :class="workflowClasses"
@touchstart="mouseDown"
@touchend="mouseUp"
@touchmove="mouseMoveNodeWorkflow"
@mousedown="mouseDown" @mousedown="mouseDown"
v-touch:tap="mouseDown"
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelScroll" @wheel="wheelScroll"
> >
<div class="node-view-background" :style="backgroundStyle"></div> <div id="node-view-background" class="node-view-background" :style="backgroundStyle"></div>
<div id="node-view" class="node-view" :style="workflowStyle"> <div id="node-view" class="node-view" :style="workflowStyle">
<node <node
v-for="nodeData in nodes" v-for="nodeData in nodes"
@ -336,14 +340,17 @@ export default mixins(
await this.addNodes(data.nodes, data.connections); await this.addNodes(data.nodes, data.connections);
}, },
mouseDown (e: MouseEvent) { mouseDown (e: MouseEvent | TouchEvent) {
// Save the location of the mouse click console.log('mouseDown');
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
this.lastClickPosition[0] = e.pageX - offsetPosition[0];
this.lastClickPosition[1] = e.pageY - offsetPosition[1];
this.mouseDownMouseSelect(e); // Save the location of the mouse click
this.mouseDownMoveWorkflow(e); const position = this.getMousePosition(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
this.lastClickPosition[0] = position.x - offsetPosition[0];
this.lastClickPosition[1] = position.y - offsetPosition[1];
this.mouseDownMouseSelect(e as MouseEvent);
this.mouseDownMoveWorkflow(e as MouseEvent);
// Hide the node-creator // Hide the node-creator
this.createNodeActive = false; this.createNodeActive = false;