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)
+ }
+ })
+