diff --git a/packages/design-system/src/composables/useDeviceSupport.test.ts b/packages/design-system/src/composables/useDeviceSupport.test.ts new file mode 100644 index 0000000000..167053c679 --- /dev/null +++ b/packages/design-system/src/composables/useDeviceSupport.test.ts @@ -0,0 +1,80 @@ +import { useDeviceSupport } from '@/composables/useDeviceSupport'; + +describe('useDeviceSupport()', () => { + beforeEach(() => { + global.window = Object.create(window); + global.navigator = { userAgent: 'test-agent', maxTouchPoints: 0 }; + }); + + describe('isTouchDevice', () => { + it('should be true if ontouchstart is in window', () => { + Object.defineProperty(window, 'ontouchstart', {}); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(true); + }); + + it('should be true if navigator.maxTouchPoints > 0', () => { + Object.defineProperty(navigator, 'maxTouchPoints', { value: 1 }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(true); + }); + + it('should be false if no touch support', () => { + delete window.ontouchstart; + Object.defineProperty(navigator, 'maxTouchPoints', { value: 0 }); + const { isTouchDevice } = useDeviceSupport(); + expect(isTouchDevice).toEqual(false); + }); + }); + + describe('isMacOs', () => { + it('should be true for macOS user agent', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { isMacOs } = useDeviceSupport(); + expect(isMacOs).toEqual(true); + }); + + it('should be false for non-macOS user agent', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { isMacOs } = useDeviceSupport(); + expect(isMacOs).toEqual(false); + }); + }); + + describe('controlKeyCode', () => { + it('should return Meta on macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { controlKeyCode } = useDeviceSupport(); + expect(controlKeyCode).toEqual('Meta'); + }); + + it('should return Control on non-macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { controlKeyCode } = useDeviceSupport(); + expect(controlKeyCode).toEqual('Control'); + }); + }); + + describe('isCtrlKeyPressed()', () => { + it('should return true for metaKey press on macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'macintosh' }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const event = new KeyboardEvent('keydown', { metaKey: true }); + expect(isCtrlKeyPressed(event)).toEqual(true); + }); + + it('should return true for ctrlKey press on non-macOS', () => { + Object.defineProperty(navigator, 'userAgent', { value: 'windows' }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const event = new KeyboardEvent('keydown', { ctrlKey: true }); + expect(isCtrlKeyPressed(event)).toEqual(true); + }); + + it('should return true for touch device on MouseEvent', () => { + Object.defineProperty(window, 'ontouchstart', { value: {} }); + const { isCtrlKeyPressed } = useDeviceSupport(); + const mockEvent = new MouseEvent('click'); + expect(isCtrlKeyPressed(mockEvent)).toEqual(true); + }); + }); +}); diff --git a/packages/design-system/src/composables/useDeviceSupport.ts b/packages/design-system/src/composables/useDeviceSupport.ts index 6296304578..01d52b890b 100644 --- a/packages/design-system/src/composables/useDeviceSupport.ts +++ b/packages/design-system/src/composables/useDeviceSupport.ts @@ -1,14 +1,7 @@ import { ref } from 'vue'; -interface DeviceSupportHelpers { - isTouchDevice: boolean; - isMacOs: boolean; - controlKeyCode: string; - isCtrlKeyPressed: (e: MouseEvent | KeyboardEvent) => boolean; -} - -export function useDeviceSupport(): DeviceSupportHelpers { - const isTouchDevice = ref('ontouchstart' in window || navigator.maxTouchPoints > 0); +export function useDeviceSupport() { + const isTouchDevice = ref(window.hasOwnProperty('ontouchstart') || navigator.maxTouchPoints > 0); const userAgent = ref(navigator.userAgent.toLowerCase()); const isMacOs = ref( userAgent.value.includes('macintosh') || diff --git a/packages/editor-ui/src/components/CanvasControls.vue b/packages/editor-ui/src/components/CanvasControls.vue index 3b6d83ae23..b5677e54f3 100644 --- a/packages/editor-ui/src/components/CanvasControls.vue +++ b/packages/editor-ui/src/components/CanvasControls.vue @@ -62,13 +62,15 @@ import { onBeforeMount, onBeforeUnmount } from 'vue'; import { storeToRefs } from 'pinia'; import { useCanvasStore } from '@/stores/canvas.store'; import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue'; +import { useDeviceSupport } from 'n8n-design-system'; const canvasStore = useCanvasStore(); const { zoomToFit, zoomIn, zoomOut, resetZoom } = canvasStore; const { nodeViewScale, isDemo } = storeToRefs(canvasStore); +const deviceSupport = useDeviceSupport(); const keyDown = (e: KeyboardEvent) => { - const isCtrlKeyPressed = e.metaKey || e.ctrlKey; + const isCtrlKeyPressed = deviceSupport.isCtrlKeyPressed(e); if ((e.key === '=' || e.key === '+') && !isCtrlKeyPressed) { zoomIn(); } else if ((e.key === '_' || e.key === '-') && !isCtrlKeyPressed) { diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 6c9be87f15..53a69cd785 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -14,7 +14,7 @@ :class="{ 'node-default': true, 'touch-active': isTouchActive, - 'is-touch-device': isTouchDevice, + 'is-touch-device': deviceSupport.isTouchDevice, 'menu-open': isContextMenuOpen, 'disable-pointer-events': disablePointerEvents, }" @@ -187,6 +187,7 @@ import { type ContextMenuTarget, useContextMenu } from '@/composables/useContext import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { usePinnedData } from '@/composables/usePinnedData'; +import { useDeviceSupport } from 'n8n-design-system'; import { useDebounce } from '@/composables/useDebounce'; export default defineComponent({ @@ -218,9 +219,17 @@ export default defineComponent({ const nodeHelpers = useNodeHelpers(); const node = workflowsStore.getNodeByName(props.name); const pinnedData = usePinnedData(node); + const deviceSupport = useDeviceSupport(); const { callDebounced } = useDebounce(); - return { contextMenu, externalHooks, nodeHelpers, pinnedData, callDebounced }; + return { + contextMenu, + externalHooks, + nodeHelpers, + pinnedData, + deviceSupport, + callDebounced, + }; }, computed: { ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), @@ -698,7 +707,7 @@ export default defineComponent({ this.pinDataDiscoveryTooltipVisible = false; }, touchStart() { - if (this.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { + if (this.deviceSupport.isTouchDevice && !this.deviceSupport.isMacOs && !this.isTouchActive) { this.isTouchActive = true; setTimeout(() => { this.isTouchActive = false; diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 88463dbaf8..e27744d387 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -169,7 +169,7 @@ import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useMessage } from '@/composables/useMessage'; import { useExternalHooks } from '@/composables/useExternalHooks'; diff --git a/packages/editor-ui/src/components/Sticky.vue b/packages/editor-ui/src/components/Sticky.vue index 5c194437d2..1042548f15 100644 --- a/packages/editor-ui/src/components/Sticky.vue +++ b/packages/editor-ui/src/components/Sticky.vue @@ -11,7 +11,7 @@ :class="{ 'sticky-default': true, 'touch-active': isTouchActive, - 'is-touch-device': isTouchDevice, + 'is-touch-device': deviceSupport.isTouchDevice, 'is-read-only': isReadOnly, }" :style="stickySize" @@ -122,6 +122,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useContextMenu } from '@/composables/useContextMenu'; +import { useDeviceSupport } from 'n8n-design-system'; export default defineComponent({ name: 'Sticky', @@ -135,6 +136,7 @@ export default defineComponent({ }, }, setup() { + const deviceSupport = useDeviceSupport(); const colorPopoverTrigger = ref(); const forceActions = ref(false); const setForceActions = (value: boolean) => { @@ -147,7 +149,7 @@ export default defineComponent({ } }); - return { colorPopoverTrigger, contextMenu, forceActions, setForceActions }; + return { deviceSupport, colorPopoverTrigger, contextMenu, forceActions, setForceActions }; }, computed: { ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), @@ -318,7 +320,7 @@ export default defineComponent({ this.workflowsStore.updateNodeProperties(updateInformation); }, touchStart() { - if (this.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { + if (this.deviceSupport.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) { this.isTouchActive = true; setTimeout(() => { this.isTouchActive = false; diff --git a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts index c7d015c01b..a04342db23 100644 --- a/packages/editor-ui/src/composables/useCanvasMouseSelect.ts +++ b/packages/editor-ui/src/composables/useCanvasMouseSelect.ts @@ -1,6 +1,6 @@ import type { INodeUi, XYPosition } from '@/Interface'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getMousePosition, getRelativePosition } from '@/utils/nodeViewUtils'; diff --git a/packages/editor-ui/src/composables/useHistoryHelper.ts b/packages/editor-ui/src/composables/useHistoryHelper.ts index b259a812eb..edba55b12d 100644 --- a/packages/editor-ui/src/composables/useHistoryHelper.ts +++ b/packages/editor-ui/src/composables/useHistoryHelper.ts @@ -6,7 +6,7 @@ import { useHistoryStore } from '@/stores/history.store'; import { useUIStore } from '@/stores/ui.store'; import { onMounted, onUnmounted, nextTick } from 'vue'; -import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; +import { useDeviceSupport } from 'n8n-design-system'; import { getNodeViewTab } from '@/utils/canvasUtils'; import type { Route } from 'vue-router'; import { useTelemetry } from './useTelemetry'; diff --git a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts deleted file mode 100644 index a8f87f8820..0000000000 --- a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineComponent } from 'vue'; - -export const deviceSupportHelpers = defineComponent({ - data() { - return { - // @ts-ignore msMaxTouchPoints is deprecated but must fix tablet bugs before fixing this.. otherwise breaks touchscreen computers - isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints, - isMacOs: /(ipad|iphone|ipod|mac)/i.test(navigator.platform), // TODO: `platform` deprecated - }; - }, - 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 && e instanceof MouseEvent) { - return true; - } - if (this.isMacOs) { - return e.metaKey; - } - return e.ctrlKey; - }, - }, -}); diff --git a/packages/editor-ui/src/mixins/moveNodeWorkflow.ts b/packages/editor-ui/src/mixins/moveNodeWorkflow.ts index 1ae6d2743a..8e91af0ab0 100644 --- a/packages/editor-ui/src/mixins/moveNodeWorkflow.ts +++ b/packages/editor-ui/src/mixins/moveNodeWorkflow.ts @@ -1,12 +1,11 @@ import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; -import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { getMousePosition } from '@/utils/nodeViewUtils'; import { useUIStore } from '@/stores/ui.store'; +import { useDeviceSupport } from 'n8n-design-system'; export const moveNodeWorkflow = defineComponent({ - mixins: [deviceSupportHelpers], data() { return { moveLastPosition: [0, 0], @@ -30,7 +29,9 @@ export const moveNodeWorkflow = defineComponent({ this.moveLastPosition[1] = y; }, mouseDownMoveWorkflow(e: MouseEvent, moveButtonPressed: boolean) { - if (!this.isCtrlKeyPressed(e) && !moveButtonPressed) { + const deviceSupport = useDeviceSupport(); + + if (!deviceSupport.isCtrlKeyPressed(e) && !moveButtonPressed) { // We only care about it when the ctrl key is pressed at the same time. // So we exit when it is not pressed. return; diff --git a/packages/editor-ui/src/mixins/nodeBase.ts b/packages/editor-ui/src/mixins/nodeBase.ts index 877c8f2765..6d9193250c 100644 --- a/packages/editor-ui/src/mixins/nodeBase.ts +++ b/packages/editor-ui/src/mixins/nodeBase.ts @@ -3,7 +3,6 @@ import type { PropType } from 'vue'; import { mapStores } from 'pinia'; import type { INodeUi } from '@/Interface'; -import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers'; import { NO_OP_NODE_TYPE, NODE_CONNECTION_TYPE_ALLOW_MULTIPLE, @@ -28,6 +27,7 @@ import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { useHistoryStore } from '@/stores/history.store'; import { useCanvasStore } from '@/stores/canvas.store'; import type { EndpointSpec } from '@jsplumb/common'; +import { useDeviceSupport } from 'n8n-design-system'; const createAddInputEndpointSpec = ( connectionName: NodeConnectionType, @@ -56,7 +56,6 @@ const createDiamondOutputEndpointSpec = (): EndpointSpec => ({ }); export const nodeBase = defineComponent({ - mixins: [deviceSupportHelpers], data() { return { inputs: [] as Array, @@ -615,13 +614,16 @@ export const nodeBase = defineComponent({ return createSupplementalConnectionType(connectionType); }, touchEnd(e: MouseEvent) { - if (this.isTouchDevice) { + const deviceSupport = useDeviceSupport(); + if (deviceSupport.isTouchDevice) { if (this.uiStore.isActionActive('dragActive')) { this.uiStore.removeActiveAction('dragActive'); } } }, mouseLeftClick(e: MouseEvent) { + const deviceSupport = useDeviceSupport(); + // @ts-ignore const path = e.path || (e.composedPath && e.composedPath()); for (let index = 0; index < path.length; index++) { @@ -634,11 +636,11 @@ export const nodeBase = defineComponent({ } } - if (!this.isTouchDevice) { + if (!deviceSupport.isTouchDevice) { if (this.uiStore.isActionActive('dragActive')) { this.uiStore.removeActiveAction('dragActive'); } else { - if (!this.isCtrlKeyPressed(e)) { + if (!deviceSupport.isCtrlKeyPressed(e)) { this.$emit('deselectAllNodes'); } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index ff69d63469..f06e4c7e31 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -377,6 +377,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { useClipboard } from '@/composables/useClipboard'; import { usePinnedData } from '@/composables/usePinnedData'; import { useSourceControlStore } from '@/stores/sourceControl.store'; +import { useDeviceSupport } from 'n8n-design-system'; import { useDebounce } from '@/composables/useDebounce'; interface AddNodeOptions { @@ -476,6 +477,7 @@ export default defineComponent({ const clipboard = useClipboard(); const { activeNode } = storeToRefs(ndvStore); const pinnedData = usePinnedData(activeNode); + const deviceSupport = useDeviceSupport(); const { callDebounced } = useDebounce(); return { @@ -486,6 +488,7 @@ export default defineComponent({ externalHooks, clipboard, pinnedData, + deviceSupport, callDebounced, ...useCanvasMouseSelect(), ...useGlobalLinkActions(), @@ -1378,7 +1381,7 @@ export default defineComponent({ this.collaborationStore.notifyWorkflowOpened(workflow.id); }, touchTap(e: MouseEvent | TouchEvent) { - if (this.isTouchDevice) { + if (this.deviceSupport.isTouchDevice) { this.mouseDown(e); } }, @@ -1403,7 +1406,7 @@ export default defineComponent({ this.mouseUpMoveWorkflow(e); }, keyUp(e: KeyboardEvent) { - if (e.key === this.controlKeyCode) { + if (e.key === this.deviceSupport.controlKeyCode) { this.ctrlKeyPressed = false; } if (e.key === ' ') { @@ -1413,10 +1416,10 @@ export default defineComponent({ async keyDown(e: KeyboardEvent) { this.contextMenu.close(); - const ctrlModifier = this.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; - const shiftModifier = e.shiftKey && !e.altKey && !this.isCtrlKeyPressed(e); - const ctrlAltModifier = this.isCtrlKeyPressed(e) && e.altKey && !e.shiftKey; - const noModifierKeys = !this.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; + const ctrlModifier = this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; + const shiftModifier = e.shiftKey && !e.altKey && !this.deviceSupport.isCtrlKeyPressed(e); + const ctrlAltModifier = this.deviceSupport.isCtrlKeyPressed(e) && e.altKey && !e.shiftKey; + const noModifierKeys = !this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; const readOnly = this.isReadOnlyRoute || this.readOnlyEnv; if (e.key === 's' && ctrlModifier && !readOnly) { @@ -1497,7 +1500,7 @@ export default defineComponent({ void this.onRunWorkflow(); } else if (e.key === 'S' && shiftModifier && !readOnly) { void this.onAddNodes({ nodes: [{ type: STICKY_NODE_TYPE }], connections: [] }); - } else if (e.key === this.controlKeyCode) { + } else if (e.key === this.deviceSupport.controlKeyCode) { this.ctrlKeyPressed = true; } else if (e.key === ' ') { this.moveCanvasKeyPressed = true;