diff --git a/electron/components/contextMenu.js b/electron/components/contextMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..6ad6bcb6960c84c11c12c344308b6d7ff628ebf1 --- /dev/null +++ b/electron/components/contextMenu.js @@ -0,0 +1,38 @@ +const { Menu } = require('electron'); +const { BrowserWindow } = require('electron'); +const { sendToFrontend } = require('./ipc'); + +const { screen } = require('electron'); + +const contextTemplate = [ + { + label: 'Ajouter', + submenu: [ + { + label: 'Ajouter du texte', + click: () => { sendToFrontend(BrowserWindow.getFocusedWindow(),'addPage', { type: 'text', pos: getRelativeCursorPosition() }); } + }, + { + label: 'Ajouter une vidéo', + click: () => { sendToFrontend(BrowserWindow.getFocusedWindow(), 'addPage', { type: 'video', pos: getRelativeCursorPosition() }); } + } + ] + }, +]; + +function getRelativeCursorPosition() { + const cursorPosition = screen.getCursorScreenPoint(); + // Get the position of the current window + const windowPosition = BrowserWindow.getFocusedWindow().getPosition(); + + // Calculate the cursor position relative to the current window + const cursorPositionRelativeToWindow = { + x: cursorPosition.x - windowPosition[0], + y: cursorPosition.y - windowPosition[1] + }; + + return cursorPositionRelativeToWindow; +} + + +module.exports.popupMenu = Menu.buildFromTemplate(contextTemplate); \ No newline at end of file diff --git a/electron/electron.js b/electron/electron.js index f0bb0e1cd7da25792bfe0dcf8098b8eb4466ee75..4c5741f132c4968e56fbeec378d07e684378bedd 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -10,6 +10,8 @@ const { cleanPreview } = require('./components/preview'); const path = require('path'); const store = require('./components/store'); +const { popupMenu } = require('./components/contextMenu'); + let mainWindow; let splashWindow; // Open file with editor, on windows : using argv | on macOS using open-file event (see below) @@ -78,4 +80,8 @@ app.whenReady().then(() => { cleanPreview(); app.quit(); }); + + mainWindow.webContents.on('context-menu', () => { + popupMenu.popup(mainWindow.webContents); + }); }); \ No newline at end of file diff --git a/src/features/ePocFlow/ePocFlow.vue b/src/features/ePocFlow/ePocFlow.vue index c1c0a7a391e5028ecb854477b9e2a54bc9c939b2..79c25410ba6ca9a5584ea254e08eb9847d8cfd37 100644 --- a/src/features/ePocFlow/ePocFlow.vue +++ b/src/features/ePocFlow/ePocFlow.vue @@ -151,6 +151,7 @@ function onEdgeclick (event) { @dragover.prevent @dragenter.prevent @edge-click="onEdgeclick" + @pane-click="editorStore.closeFormPanel()" > <template #node-custom="{ id, data }"> <ScreenNode :id="id" :data="data" /> diff --git a/src/features/ePocFlow/nodes/ChapterNode.vue b/src/features/ePocFlow/nodes/ChapterNode.vue index 8f08d4190e450efd628fa0e5c23701177458f6a4..6b368fc6d076e8bbb5fdca6ecfbdaff0757ce264 100644 --- a/src/features/ePocFlow/nodes/ChapterNode.vue +++ b/src/features/ePocFlow/nodes/ChapterNode.vue @@ -42,6 +42,7 @@ const subtitle = computed(() => { :class-list="{ 'btn-content-blue' : false, 'clickable': true, 'btn-content-node': true, 'btn-content-large': true }" :subtitle="subtitle" @click="openForm()" + @mousedown="editorStore.closeFormPanel()" /> </div> <Handle diff --git a/src/features/ePocFlow/nodes/ScreenNode.vue b/src/features/ePocFlow/nodes/ScreenNode.vue index 24a35cdfbd05ebb78633e2e7a1175c52c3d98576..45ef4d94ae13bfa48d76c041be99ff61876c7ef9 100644 --- a/src/features/ePocFlow/nodes/ScreenNode.vue +++ b/src/features/ePocFlow/nodes/ScreenNode.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> import { Handle, Position, useVueFlow } from '@vue-flow/core'; import ContentButton from '@/src/components/ContentButton.vue'; -import { computed, onMounted, ref } from 'vue'; +import { onMounted, ref } from 'vue'; import { useEditorStore } from '@/src/shared/stores'; import { NodeElement } from '@/src/shared/interfaces'; @@ -21,7 +21,7 @@ const props = defineProps<{ }>(); -const { findNode, getSelectedNodes } = useVueFlow({ id: 'main' }); +const { findNode } = useVueFlow({ id: 'main' }); const node = findNode(props.id); const dropped = ref(false); @@ -116,14 +116,10 @@ function dragStart(event, element: NodeElement, index: number) { event.dataTransfer.setData('source', JSON.stringify({ parent: props.id, index: index})); } -function cmdClick() { +function closeFormPanel() { editorStore.closeFormPanel(); } -const isSelected = computed(() => { - return getSelectedNodes.value.find((selectedNode) => selectedNode.id === node.id); -}); - </script> <template> @@ -138,11 +134,12 @@ const isSelected = computed(() => { /> <div :id="'node'+ props.id" - :class=" { 'active': editorStore.openedNodeId ? editorStore.openedNodeId === props.id : false, 'selected': isSelected }" + :class=" { 'active': editorStore.openedNodeId ? editorStore.openedNodeId === props.id : false }" class="node" @click.exact="openPageForm(node.id, node.data.formType, node.data.formValues)" - @click.meta="cmdClick" - @click.ctrl="cmdClick" + @click.meta="closeFormPanel" + @click.ctrl="closeFormPanel" + @mousedown="closeFormPanel" @dragover="dragOver($event)" @dragleave="dragLeave($event)" @dragenter="dragEnter($event)" @@ -166,8 +163,8 @@ const isSelected = computed(() => { :is-draggable="isQuestion" :class-list="{ 'btn-content-blue' : false, 'clickable': true, 'btn-content-node': true }" @click.exact="openForm(element)" - @click.meta="cmdClick" - @click.ctrl="cmdClick" + @click.meta="closeFormPanel" + @click.ctrl="closeFormPanel" @dragstart="dragStart($event, element, index)" /> </div> diff --git a/src/features/ePocFlow/nodes/ePocNode.vue b/src/features/ePocFlow/nodes/ePocNode.vue index 613b6df56e5b4fbd5894a4931fa54f1653383737..3bc2a294fe78dbd9839331a5806bb635d5258dc1 100644 --- a/src/features/ePocFlow/nodes/ePocNode.vue +++ b/src/features/ePocFlow/nodes/ePocNode.vue @@ -31,6 +31,7 @@ function openForm() { :class-list="{ 'btn-content-blue' : false, 'clickable': true, 'btn-content-node': true, 'btn-content-large': true }" subtitle="ePoc" @click="openForm()" + @mousedown="editorStore.closeFormPanel()" /> </div> </template> \ No newline at end of file diff --git a/src/global.scss b/src/global.scss index 8858dfee1242f9e054e747eaa40c75ecbd90ffac..5b47d1c88964d866ca819731440ad73b5a18dcdf 100644 --- a/src/global.scss +++ b/src/global.scss @@ -229,11 +229,6 @@ hr { box-shadow: 0 1px 8px 0 var(--editor-blue-shadow); background-color: var(--node-active); } - - &.selected { - border: 1px solid var(--editor-blue); - box-shadow: 0 1px 8px 0 var(--editor-blue-shadow); - } &-animate { padding: 1.5rem; @@ -472,4 +467,11 @@ g.selected { .vue-flow__nodesselection-rect { padding: 2rem 1rem 1rem 1rem; transform: translate(-1rem, -2rem); +} + +.vue-flow__node.selected { + .node { + border: 1px solid var(--editor-blue); + box-shadow: 0 1px 8px 0 var(--editor-blue-shadow); + } } \ No newline at end of file diff --git a/src/shared/services/editor.service.ts b/src/shared/services/editor.service.ts index 636581712466b69a4912396bc90a335f378d519b..5a11178061e668ec745deaac1d76ab224a0dee78 100644 --- a/src/shared/services/editor.service.ts +++ b/src/shared/services/editor.service.ts @@ -122,6 +122,11 @@ const setup = function () { editorStore.exporting = false; }); + api.receive('addPage', (data: string) => { + const page = JSON.parse(data); + editorStore.addNewPage(page.type, page.pos); + }); + initialized = true; }; function newEpocProject(): void { diff --git a/src/shared/stores/editorStore.ts b/src/shared/stores/editorStore.ts index bb3d894bd971beaad664c7112e4f4e19223e68e2..b448b0a00e2ebf0431aa7aec5e6ed475aff10f4a 100644 --- a/src/shared/stores/editorStore.ts +++ b/src/shared/stores/editorStore.ts @@ -1,11 +1,11 @@ import { defineStore } from 'pinia'; -import { ePocProject, Form, FormButton, Screen, SideAction } from '@/src/shared/interfaces'; -import { toRaw } from 'vue'; +import { ePocProject, Form, FormButton, NodeElement, Screen, SideAction } from '@/src/shared/interfaces'; +import { nextTick, toRaw, watch } from 'vue'; import { applyNodeChanges, getConnectedEdges, useVueFlow } from '@vue-flow/core'; import { formsModel, questions, standardScreen } from '@/src/shared/data/form.data'; -const { findNode, nodes, edges, addNodes } = useVueFlow({ id: 'main' }); +const { findNode, nodes, edges, addNodes, project, vueFlowRef } = useVueFlow({ id: 'main' }); type uid = string; @@ -72,7 +72,9 @@ export const useEditorStore = defineStore('editor', { //? To be sure the view is notified of closing / reopening setTimeout(() => { this.formPanel = structuredClone(formsModel.find(form => form.type === formType)); - document.querySelectorAll('.node.selected').forEach(node => node.classList.remove('selected')); + }); + nodes.value.forEach((node) => { + node.selected = false; }); }, closeFormPanel(): void { @@ -248,6 +250,54 @@ export const useEditorStore = defineStore('editor', { node.data.elements.push(newElement); this.addElementToScreen(node.id, newElement.action); + }, + addNewPage(type: string, pos: { x: number, y: number }) { + const types = standardScreen.concat(questions); + + const elements: NodeElement[] = []; + const id = this.generateId(); + + elements.push({ + id: this.generateId(), + action: types.find((value) => value.type === type), + formType: type, + formValues: {}, + parentId: id, + contentId: this.generateContentId(), + }); + + const { left, top } = vueFlowRef.value.getBoundingClientRect(); + + const position = project({ + x: pos.x - left, + y: pos.y - top, + }); + + const newNode = { + id: id, + type: 'content', + data: { type: type, readyToDrop: false, animated: false, elements: elements, formType: 'screen', formValues: {}, contentId: id }, + position: position, + deletable: false + }; + + addNodes([newNode]); + + nextTick(() => { + const node = findNode(newNode.id); + const stop = watch( + () => node.dimensions, + (dimensions) => { + if (dimensions.width > 0 && dimensions.height > 0) { + node.position = { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 }; + stop(); + } + }, + { deep: true, flush: 'post' }, + ); + }); + + this.addElementToScreen(id, elements[0].action); } } });