diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 58d803aa03..ff926bce1f 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -618,6 +618,16 @@ export const getFrontendStore = () => { } } + // Check inside is valid + if (mode === "inside") { + const definition = store.actions.components.getDefinition( + targetComponent._component + ) + if (!definition.hasChildren) { + mode = "below" + } + } + // Paste new component if (mode === "inside") { // Paste inside target component if chosen @@ -654,46 +664,197 @@ export const getFrontendStore = () => { return state }) }, + selectPrevious: () => { + const state = get(store) + const componentId = state.selectedComponentId + const screen = get(selectedScreen) + const parent = findComponentParent(screen.props, componentId) + let newComponentId = componentId + + // Check we aren't right at the top of the tree + const index = parent?._children.findIndex(x => x._id === componentId) + if (!parent || componentId === screen.props._id) { + return + } + + // If we have siblings above us, choose the sibling or a descendant + if (index > 0) { + // If sibling before us accepts children, select a descendant + const previousSibling = parent._children[index - 1] + if (previousSibling._children?.length) { + let target = previousSibling + while (target._children?.length) { + target = target._children[target._children.length - 1] + } + newComponentId = target._id + } + + // Otherwise just select sibling + else { + newComponentId = previousSibling._id + } + } + + // If no siblings above us, select the parent + else { + newComponentId = parent._id + } + + // Only update state if component changed + if (newComponentId !== componentId) { + store.update(state => { + state.selectedComponentId = newComponentId + return state + }) + } + }, + selectNext: () => { + const component = get(selectedComponent) + const componentId = component?._id + const screen = get(selectedScreen) + const parent = findComponentParent(screen.props, componentId) + const index = parent?._children.findIndex(x => x._id === componentId) + let newComponentId = componentId + + // If we have children, select first child + if (component._children?.length) { + newComponentId = component._children[0]._id + } else if (!parent) { + return null + } + + // Otherwise select the next sibling if we have one + else if (index < parent._children.length - 1) { + const nextSibling = parent._children[index + 1] + newComponentId = nextSibling._id + } + + // Last child, select our parents next sibling + else { + let target = parent + let targetParent = findComponentParent(screen.props, target._id) + let targetIndex = targetParent?._children.findIndex( + child => child._id === target._id + ) + while ( + targetParent != null && + targetIndex === targetParent._children?.length - 1 + ) { + target = targetParent + targetParent = findComponentParent(screen.props, target._id) + targetIndex = targetParent?._children.findIndex( + child => child._id === target._id + ) + } + if (targetParent) { + newComponentId = targetParent._children[targetIndex + 1]._id + } + } + + // Only update state if component ID is different + if (newComponentId !== componentId) { + store.update(state => { + state.selectedComponentId = newComponentId + return state + }) + } + }, moveUp: async component => { await store.actions.screens.patch(screen => { const componentId = component?._id const parent = findComponentParent(screen.props, componentId) - if (!parent?._children?.length) { - return false + + // Check we aren't right at the top of the tree + const index = parent?._children.findIndex(x => x._id === componentId) + if (!parent || (index === 0 && parent._id === screen.props._id)) { + return } - const currentIndex = parent._children.findIndex( - child => child._id === componentId - ) - if (currentIndex === 0) { - return false - } - const originalComponent = cloneDeep(parent._children[currentIndex]) - const newChildren = parent._children.filter( + + // Copy original component and remove it from the parent + const originalComponent = cloneDeep(parent._children[index]) + parent._children = parent._children.filter( component => component._id !== componentId ) - newChildren.splice(currentIndex - 1, 0, originalComponent) - parent._children = newChildren + + // If we have siblings above us, move up + if (index > 0) { + // If sibling before us accepts children, move to last child of + // sibling + const previousSibling = parent._children[index - 1] + const definition = store.actions.components.getDefinition( + previousSibling._component + ) + if (definition.hasChildren) { + previousSibling._children.push(originalComponent) + } + + // Otherwise just move component above sibling + else { + parent._children.splice(index - 1, 0, originalComponent) + } + } + + // If no siblings above us, go above the parent as long as it isn't + // the screen + else if (parent._id !== screen.props._id) { + const grandParent = findComponentParent(screen.props, parent._id) + const parentIndex = grandParent._children.findIndex( + child => child._id === parent._id + ) + grandParent._children.splice(parentIndex, 0, originalComponent) + } }) }, moveDown: async component => { await store.actions.screens.patch(screen => { const componentId = component?._id const parent = findComponentParent(screen.props, componentId) + + // Sanity check parent is found if (!parent?._children?.length) { return false } - const currentIndex = parent._children.findIndex( - child => child._id === componentId - ) - if (currentIndex === parent._children.length - 1) { - return false + + // Check we aren't right at the bottom of the tree + const index = parent._children.findIndex(x => x._id === componentId) + if ( + index === parent._children.length - 1 && + parent._id === screen.props._id + ) { + return } - const originalComponent = cloneDeep(parent._children[currentIndex]) - const newChildren = parent._children.filter( + + // Copy the original component and remove from parent + const originalComponent = cloneDeep(parent._children[index]) + parent._children = parent._children.filter( component => component._id !== componentId ) - newChildren.splice(currentIndex + 1, 0, originalComponent) - parent._children = newChildren + + // Move below the next sibling if we are not the last sibling + if (index < parent._children.length) { + // If the next sibling has children, become the first child + const nextSibling = parent._children[index] + const definition = store.actions.components.getDefinition( + nextSibling._component + ) + if (definition.hasChildren) { + nextSibling._children.splice(0, 0, originalComponent) + } + + // Otherwise move below next sibling + else { + parent._children.splice(index + 1, 0, originalComponent) + } + } + + // Last child, so move below our parent + else { + const grandParent = findComponentParent(screen.props, parent._id) + const parentIndex = grandParent._children.findIndex( + child => child._id === parent._id + ) + grandParent._children.splice(parentIndex + 1, 0, originalComponent) + } }) }, updateStyle: async (name, value) => { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index abb956c9d3..dc22f93300 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -144,7 +144,11 @@ } else if (type === "update-prop") { await store.actions.components.updateSetting(data.prop, data.value) } else if (type === "delete-component" && data.id) { + // Legacy type, can be deleted in future confirmDeleteComponent(data.id) + } else if (type === "key-down") { + const { key, ctrlKey } = data + document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey })) } else if (type === "duplicate-component" && data.id) { const rootComponent = get(currentAsset).props const component = findComponent(rootComponent, data.id) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte index 1bb4e3d9cd..e10015f964 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte @@ -2,16 +2,19 @@ import Panel from "components/design/Panel.svelte" import ComponentTree from "./ComponentTree.svelte" import { dndStore } from "./dndStore.js" - import { goto } from "@roxi/routify" - import { store, selectedScreen } from "builderStore" + import { goto, isActive } from "@roxi/routify" + import { store, selectedScreen, selectedComponent } from "builderStore" import NavItem from "components/common/NavItem.svelte" import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte" - import { setContext } from "svelte" + import { setContext, onMount } from "svelte" + import { get } from "svelte/store" import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import { DropPosition } from "./dndStore" import { notifications } from "@budibase/bbui" + import ConfirmDialog from "components/common/ConfirmDialog.svelte" let scrollRef + let confirmDeleteDialog const scrollTo = bounds => { if (!bounds) { @@ -69,6 +72,69 @@ setContext("scroll", { scrollTo, }) + + const deleteComponent = async () => { + await store.actions.components.delete(get(selectedComponent)) + } + + const handleKeyPress = async e => { + // Ignore repeating events + if (e.repeat) { + return + } + // Ignore events when typing + const activeTag = document.activeElement?.tagName.toLowerCase() + if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") { + return + } + const component = get(selectedComponent) + try { + if (e.key === "Delete") { + e.preventDefault() + confirmDeleteDialog.show() + } else if (e.ctrlKey) { + if (e.key === "ArrowUp") { + e.preventDefault() + e.stopPropagation() + await store.actions.components.moveUp(component) + } else if (e.key === "ArrowDown") { + e.preventDefault() + await store.actions.components.moveDown(component) + } else if (e.key === "c") { + e.preventDefault() + await store.actions.components.copy(component, false) + } else if (e.key === "x") { + e.preventDefault() + store.actions.components.copy(component, true) + } else if (e.key === "v") { + e.preventDefault() + await store.actions.components.paste(component, "inside") + } else if (e.key === "Enter") { + e.preventDefault() + $goto("./new") + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + await store.actions.components.selectPrevious() + } else if (e.key === "ArrowDown") { + e.preventDefault() + await store.actions.components.selectNext() + } else if (e.key === "Escape" && $isActive("./new")) { + e.preventDefault() + $goto("./") + } + } catch (error) { + console.log(error) + notifications.error("Error handling key press") + } + } + + onMount(() => { + document.addEventListener("keydown", handleKeyPress) + return () => { + document.removeEventListener("keydown", handleKeyPress) + } + }) +