Add keyboard shortcuts for components. Improve component reordering
This commit is contained in:
parent
8d9794df5c
commit
a4847a471c
|
@ -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
|
// Paste new component
|
||||||
if (mode === "inside") {
|
if (mode === "inside") {
|
||||||
// Paste inside target component if chosen
|
// Paste inside target component if chosen
|
||||||
|
@ -654,46 +664,197 @@ export const getFrontendStore = () => {
|
||||||
return state
|
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 => {
|
moveUp: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
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
|
// Copy original component and remove it from the parent
|
||||||
)
|
const originalComponent = cloneDeep(parent._children[index])
|
||||||
if (currentIndex === 0) {
|
parent._children = parent._children.filter(
|
||||||
return false
|
|
||||||
}
|
|
||||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
|
||||||
const newChildren = parent._children.filter(
|
|
||||||
component => component._id !== componentId
|
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 => {
|
moveDown: async component => {
|
||||||
await store.actions.screens.patch(screen => {
|
await store.actions.screens.patch(screen => {
|
||||||
const componentId = component?._id
|
const componentId = component?._id
|
||||||
const parent = findComponentParent(screen.props, componentId)
|
const parent = findComponentParent(screen.props, componentId)
|
||||||
|
|
||||||
|
// Sanity check parent is found
|
||||||
if (!parent?._children?.length) {
|
if (!parent?._children?.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const currentIndex = parent._children.findIndex(
|
|
||||||
child => child._id === componentId
|
// Check we aren't right at the bottom of the tree
|
||||||
)
|
const index = parent._children.findIndex(x => x._id === componentId)
|
||||||
if (currentIndex === parent._children.length - 1) {
|
if (
|
||||||
return false
|
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
|
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) => {
|
updateStyle: async (name, value) => {
|
||||||
|
|
|
@ -144,7 +144,11 @@
|
||||||
} else if (type === "update-prop") {
|
} else if (type === "update-prop") {
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
} else if (type === "delete-component" && data.id) {
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
// Legacy type, can be deleted in future
|
||||||
confirmDeleteComponent(data.id)
|
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) {
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
const rootComponent = get(currentAsset).props
|
const rootComponent = get(currentAsset).props
|
||||||
const component = findComponent(rootComponent, data.id)
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
|
|
@ -2,16 +2,19 @@
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import { dndStore } from "./dndStore.js"
|
import { dndStore } from "./dndStore.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
import { store, selectedScreen } from "builderStore"
|
import { store, selectedScreen, selectedComponent } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.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 DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
import { DropPosition } from "./dndStore"
|
import { DropPosition } from "./dndStore"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
let scrollRef
|
let scrollRef
|
||||||
|
let confirmDeleteDialog
|
||||||
|
|
||||||
const scrollTo = bounds => {
|
const scrollTo = bounds => {
|
||||||
if (!bounds) {
|
if (!bounds) {
|
||||||
|
@ -69,6 +72,69 @@
|
||||||
setContext("scroll", {
|
setContext("scroll", {
|
||||||
scrollTo,
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
|
@ -119,6 +185,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
body={`Are you sure you want to delete "${$selectedComponent?._instanceName}"?`}
|
||||||
|
okText="Delete Component"
|
||||||
|
onOk={deleteComponent}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.nav-items-container {
|
.nav-items-container {
|
||||||
|
|
|
@ -16,20 +16,14 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const onKeyDown = e => {
|
const onKeyDown = e => {
|
||||||
if (e.key === "Delete" || e.key === "Backspace") {
|
|
||||||
deleteSelectedComponent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteSelectedComponent = () => {
|
|
||||||
const state = get(builderStore)
|
const state = get(builderStore)
|
||||||
if (!state.inBuilder || !state.selectedComponentId || state.editMode) {
|
if (!state.inBuilder || state.editMode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
if (["input", "textarea"].indexOf(activeTag) !== -1) {
|
if (["input", "textarea"].indexOf(activeTag) !== -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
builderStore.actions.deleteComponent(state.selectedComponentId)
|
builderStore.actions.keyDown(e.key, e.ctrlKey)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -39,8 +39,8 @@ const createBuilderStore = () => {
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
dispatchEvent("update-prop", { prop, value })
|
dispatchEvent("update-prop", { prop, value })
|
||||||
},
|
},
|
||||||
deleteComponent: id => {
|
keyDown: (key, ctrlKey) => {
|
||||||
dispatchEvent("delete-component", { id })
|
dispatchEvent("key-down", { key, ctrlKey })
|
||||||
},
|
},
|
||||||
duplicateComponent: id => {
|
duplicateComponent: id => {
|
||||||
dispatchEvent("duplicate-component", { id })
|
dispatchEvent("duplicate-component", { id })
|
||||||
|
|
Loading…
Reference in New Issue