mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
⚡ Improve support for touch-devices #1070
This commit is contained in:
parent
ada485ed5c
commit
4df74771e3
|
@ -25,7 +25,8 @@
|
|||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^8.1.0"
|
||||
"uuid": "^8.1.0",
|
||||
"vue2-touch-events": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@beyonk/google-fonts-webpack-plugin": "^1.2.3",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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">
|
||||
<el-tooltip placement="top" effect="light">
|
||||
<div slot="content" v-html="nodeIssues"></div>
|
||||
|
@ -13,19 +13,19 @@
|
|||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
<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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -2,20 +2,19 @@ import { INodeUi } from '@/Interface';
|
|||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
|
||||
export const mouseSelect = mixins(nodeIndex).extend({
|
||||
export const mouseSelect = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
data () {
|
||||
return {
|
||||
selectActive: false,
|
||||
selectBox: document.createElement('span'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMacOs (): boolean {
|
||||
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.createSelectBox();
|
||||
},
|
||||
|
@ -34,6 +33,9 @@ export const mouseSelect = mixins(nodeIndex).extend({
|
|||
this.$el.appendChild(this.selectBox);
|
||||
},
|
||||
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
|
||||
if (this.isTouchDevice === true) {
|
||||
return true;
|
||||
}
|
||||
if (this.isMacOs) {
|
||||
return e.metaKey;
|
||||
}
|
||||
|
@ -125,6 +127,13 @@ export const mouseSelect = mixins(nodeIndex).extend({
|
|||
},
|
||||
mouseUpMouseSelect (e: MouseEvent) {
|
||||
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.
|
||||
// Else normal node dragging will not work.
|
||||
return;
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
|
||||
export const moveNodeWorkflow = mixins(nodeIndex).extend({
|
||||
export const moveNodeWorkflow = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
data () {
|
||||
return {
|
||||
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: {
|
||||
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
|
||||
if (this.isMacOs) {
|
||||
return e.metaKey;
|
||||
}
|
||||
return e.ctrlKey;
|
||||
getMousePosition(e: MouseEvent | TouchEvent) {
|
||||
// @ts-ignore
|
||||
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
|
||||
// @ts-ignore
|
||||
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) {
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]);
|
||||
const position = this.getMousePosition(e);
|
||||
|
||||
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
|
||||
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
|
||||
this.$store.commit('setNodeViewOffsetPosition', [nodeViewOffsetPositionX, nodeViewOffsetPositionY]);
|
||||
|
||||
// Update the last position
|
||||
this.moveLastPosition[0] = e.pageX;
|
||||
this.moveLastPosition[1] = e.pageY;
|
||||
this.moveLastPosition[0] = position.x;
|
||||
this.moveLastPosition[1] = position.y;
|
||||
},
|
||||
mouseDownMoveWorkflow (e: MouseEvent) {
|
||||
if (this.isCtrlKeyPressed(e) === false) {
|
||||
|
@ -51,8 +52,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
|
|||
|
||||
this.$store.commit('setNodeViewMoveInProgress', true);
|
||||
|
||||
this.moveLastPosition[0] = e.pageX;
|
||||
this.moveLastPosition[1] = e.pageY;
|
||||
const position = this.getMousePosition(e);
|
||||
|
||||
this.moveLastPosition[0] = position.x;
|
||||
this.moveLastPosition[1] = position.y;
|
||||
|
||||
// @ts-ignore
|
||||
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
|
||||
},
|
||||
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) {
|
||||
// Mouse button is not pressed anymore so stop selection mode
|
||||
// Happens normally when mouse leave the view pressed and then
|
||||
|
|
|
@ -2,20 +2,20 @@ import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interfac
|
|||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||
import { NODE_NAME_PREFIX } from '@/constants';
|
||||
|
||||
export const nodeBase = mixins(nodeIndex).extend({
|
||||
export const nodeBase = mixins(
|
||||
deviceSupportHelpers,
|
||||
nodeIndex,
|
||||
).extend({
|
||||
mounted () {
|
||||
// Initialize the node
|
||||
if (this.data !== null) {
|
||||
this.__addNode(this.data);
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
data (): INodeUi {
|
||||
return this.$store.getters.nodeByName(this.name);
|
||||
|
@ -26,9 +26,6 @@ export const nodeBase = mixins(nodeIndex).extend({
|
|||
}
|
||||
return false;
|
||||
},
|
||||
isMacOs (): boolean {
|
||||
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
|
||||
},
|
||||
nodeName (): string {
|
||||
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) {
|
||||
if (this.$store.getters.isActionActive('dragActive')) {
|
||||
this.$store.commit('removeActiveAction', 'dragActive');
|
||||
|
|
|
@ -5,6 +5,7 @@ import Vue from 'vue';
|
|||
import 'prismjs';
|
||||
import 'prismjs/themes/prism.css';
|
||||
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||
import Vue2TouchEvents from 'vue2-touch-events';
|
||||
|
||||
import * as ElementUI from 'element-ui';
|
||||
// @ts-ignore
|
||||
|
@ -91,6 +92,9 @@ import {
|
|||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
import { store } from './store';
|
||||
|
||||
Vue.use(Vue2TouchEvents);
|
||||
|
||||
Vue.use(ElementUI, { locale });
|
||||
|
||||
library.add(faAngleDoubleLeft);
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
<div
|
||||
class="node-view-wrapper"
|
||||
:class="workflowClasses"
|
||||
@touchstart="mouseDown"
|
||||
@touchend="mouseUp"
|
||||
@touchmove="mouseMoveNodeWorkflow"
|
||||
@mousedown="mouseDown"
|
||||
v-touch:tap="mouseDown"
|
||||
@mouseup="mouseUp"
|
||||
@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">
|
||||
<node
|
||||
v-for="nodeData in nodes"
|
||||
|
@ -336,14 +340,17 @@ export default mixins(
|
|||
|
||||
await this.addNodes(data.nodes, data.connections);
|
||||
},
|
||||
mouseDown (e: MouseEvent) {
|
||||
// Save the location of the mouse click
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
this.lastClickPosition[0] = e.pageX - offsetPosition[0];
|
||||
this.lastClickPosition[1] = e.pageY - offsetPosition[1];
|
||||
mouseDown (e: MouseEvent | TouchEvent) {
|
||||
console.log('mouseDown');
|
||||
|
||||
this.mouseDownMouseSelect(e);
|
||||
this.mouseDownMoveWorkflow(e);
|
||||
// Save the location of the mouse click
|
||||
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
|
||||
this.createNodeActive = false;
|
||||
|
@ -1680,7 +1687,7 @@ export default mixins(
|
|||
const createNodes: INode[] = [];
|
||||
|
||||
await this.loadNodesProperties(data.nodes.map(node => node.type));
|
||||
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
if (nodeTypesCount[node.type] !== undefined) {
|
||||
if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) {
|
||||
|
|
Loading…
Reference in a new issue