EditorLocalCtxMenu.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <template>
  2. <ui-popover
  3. v-model="state.show"
  4. :options="state.position"
  5. padding="p-3"
  6. @close="clearContextMenu"
  7. >
  8. <ui-list class="space-y-1 w-52">
  9. <ui-list-item
  10. v-for="item in state.items"
  11. :key="item.id"
  12. v-close-popover
  13. class="cursor-pointer text-sm justify-between"
  14. @click="item.event"
  15. >
  16. <span>
  17. {{ item.name }}
  18. </span>
  19. <span
  20. v-if="item.shortcut"
  21. class="text-sm capitalize text-gray-600 dark:text-gray-200"
  22. >
  23. {{ item.shortcut }}
  24. </span>
  25. </ui-list-item>
  26. </ui-list>
  27. </ui-popover>
  28. </template>
  29. <script setup>
  30. import { onMounted, reactive, markRaw } from 'vue';
  31. import { useI18n } from 'vue-i18n';
  32. import { excludeGroupBlocks } from '@/utils/shared';
  33. import { getReadableShortcut, getShortcut } from '@/composable/shortcut';
  34. const props = defineProps({
  35. editor: {
  36. type: Object,
  37. default: () => ({}),
  38. },
  39. packageIo: Boolean,
  40. isPackage: Boolean,
  41. });
  42. const emit = defineEmits([
  43. 'copy',
  44. 'paste',
  45. 'group',
  46. 'ungroup',
  47. 'saveBlock',
  48. 'duplicate',
  49. 'packageIo',
  50. ]);
  51. const { t } = useI18n();
  52. const state = reactive({
  53. show: false,
  54. items: [],
  55. position: {},
  56. });
  57. let ctxData = null;
  58. const menuItems = {
  59. paste: {
  60. id: 'paste',
  61. name: t('workflow.editor.paste'),
  62. icon: 'riFileCopyLine',
  63. shortcut: getReadableShortcut('mod+v'),
  64. event: () => emit('paste', ctxData?.position),
  65. },
  66. delete: {
  67. id: 'delete',
  68. name: t('common.delete'),
  69. icon: 'riDeleteBin7Line',
  70. shortcut: 'Del',
  71. event: () => {
  72. props.editor.removeEdges(ctxData.edges);
  73. props.editor.removeNodes(ctxData.nodes);
  74. },
  75. },
  76. saveToFolder: {
  77. id: 'saveToFolder',
  78. name: t('packages.set'),
  79. event: () => {
  80. emit('saveBlock', ctxData);
  81. },
  82. },
  83. copy: {
  84. id: 'copy',
  85. name: t('workflow.editor.copy'),
  86. icon: 'riFileCopyLine',
  87. event: () => emit('copy', ctxData),
  88. shortcut: getReadableShortcut('mod+c'),
  89. },
  90. group: {
  91. id: 'group',
  92. name: t('workflow.editor.group'),
  93. icon: 'riFolderZipLine',
  94. event: () => emit('group', ctxData),
  95. },
  96. ungroup: {
  97. id: 'ungroup',
  98. name: t('workflow.editor.ungroup'),
  99. icon: 'riFolderOpenLine',
  100. event: () => emit('ungroup', ctxData),
  101. },
  102. duplicate: {
  103. id: 'duplicate',
  104. name: t('workflow.editor.duplicate'),
  105. icon: 'riFileCopyLine',
  106. event: () => emit('duplicate', ctxData),
  107. shortcut: getShortcut('editor:duplicate-block').readable,
  108. },
  109. setAsInput: {
  110. id: 'setAsInput',
  111. name: 'Set as block input',
  112. event: () => emit('packageIo', { type: 'inputs', ...ctxData }),
  113. },
  114. setAsOutput: {
  115. id: 'setAsOutput',
  116. name: 'Set as block output',
  117. event: () => emit('packageIo', { type: 'outputs', ...ctxData }),
  118. },
  119. };
  120. /* eslint-disable-next-line */
  121. function showCtxMenu(items = [], event) {
  122. event.preventDefault();
  123. const { clientX, clientY } = event;
  124. if (props.isPackage && items.includes('saveToFolder')) {
  125. items.splice(items.indexOf('saveToFolder'), 1);
  126. }
  127. state.items = items.map((key) => markRaw(menuItems[key]));
  128. state.items.unshift(markRaw(menuItems.paste));
  129. state.position = {
  130. getReferenceClientRect: () => ({
  131. width: 0,
  132. height: 0,
  133. top: clientY,
  134. right: clientX,
  135. bottom: clientY,
  136. left: clientX,
  137. }),
  138. };
  139. state.show = true;
  140. }
  141. function clearContextMenu() {
  142. state.show = false;
  143. state.items = [];
  144. state.position = {};
  145. }
  146. onMounted(() => {
  147. props.editor.onNodeContextMenu(({ event, node }) => {
  148. const items = ['copy', 'duplicate', 'saveToFolder', 'delete'];
  149. if (node.label === 'blocks-group') {
  150. items.splice(3, 0, 'ungroup');
  151. } else if (!excludeGroupBlocks.includes(node.label)) {
  152. items.splice(3, 0, 'group');
  153. }
  154. const currCtxData = {
  155. edges: [],
  156. nodes: [node],
  157. position: { clientX: event.clientX, clientY: event.clientY },
  158. };
  159. if (
  160. props.isPackage &&
  161. props.packageIo &&
  162. event.target.closest('[data-handleid]')
  163. ) {
  164. const { handleid, nodeid } = event.target.dataset;
  165. currCtxData.nodeId = nodeid;
  166. currCtxData.handleId = handleid;
  167. items.unshift(
  168. event.target.classList.contains('source') ? 'setAsOutput' : 'setAsInput'
  169. );
  170. }
  171. showCtxMenu(items, event);
  172. ctxData = currCtxData;
  173. });
  174. props.editor.onEdgeContextMenu(({ event, edge }) => {
  175. showCtxMenu(['delete'], event);
  176. ctxData = { nodes: [], edges: [edge] };
  177. });
  178. props.editor.onPaneContextMenu((event) => {
  179. showCtxMenu([], event);
  180. ctxData = {
  181. nodes: [],
  182. edges: [],
  183. position: { clientX: event.clientX, clientY: event.clientY },
  184. };
  185. });
  186. props.editor.onSelectionContextMenu(({ event }) => {
  187. showCtxMenu(
  188. ['copy', 'duplicate', 'saveToFolder', 'group', 'delete'],
  189. event
  190. );
  191. ctxData = {
  192. nodes: props.editor.getSelectedNodes.value,
  193. edges: props.editor.getSelectedEdges.value,
  194. position: { clientX: event.clientX, clientY: event.clientY },
  195. };
  196. });
  197. });
  198. </script>