Merge pull request #7568 from Budibase/cheeks-fixes
Component action fixes
This commit is contained in:
commit
b49872bea3
|
@ -16,6 +16,7 @@
|
||||||
export let scrollable = false
|
export let scrollable = false
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
|
export let id
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
ondragover="return false"
|
ondragover="return false"
|
||||||
ondragenter="return false"
|
ondragenter="return false"
|
||||||
|
{id}
|
||||||
>
|
>
|
||||||
<div class="nav-item-content" bind:this={contentRef}>
|
<div class="nav-item-content" bind:this={contentRef}>
|
||||||
{#if withArrow}
|
{#if withArrow}
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
const keyboardEvent = (key, ctrlKey = false) => {
|
const keyboardEvent = (key, ctrlKey = false) => {
|
||||||
// Ensure this component is selected first
|
document.dispatchEvent(
|
||||||
if (component._id !== $store.selectedComponentId) {
|
new CustomEvent("component-menu", {
|
||||||
store.update(state => {
|
detail: {
|
||||||
state.selectedComponentId = component._id
|
key,
|
||||||
return state
|
ctrlKey,
|
||||||
|
id: component?._id,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
)
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { selectedComponent, selectedScreen, store } from "builderStore"
|
||||||
|
import { findComponent } from "builderStore/componentUtils"
|
||||||
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
let confirmDeleteDialog
|
||||||
|
let componentToDelete
|
||||||
|
|
||||||
|
const keyHandlers = {
|
||||||
|
["^ArrowUp"]: async component => {
|
||||||
|
await store.actions.components.moveUp(component)
|
||||||
|
},
|
||||||
|
["^ArrowDown"]: async component => {
|
||||||
|
await store.actions.components.moveDown(component)
|
||||||
|
},
|
||||||
|
["^c"]: component => {
|
||||||
|
store.actions.components.copy(component, false)
|
||||||
|
},
|
||||||
|
["^x"]: component => {
|
||||||
|
store.actions.components.copy(component, true)
|
||||||
|
},
|
||||||
|
["^v"]: async component => {
|
||||||
|
await store.actions.components.paste(component, "inside")
|
||||||
|
},
|
||||||
|
["^d"]: async component => {
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component, "below")
|
||||||
|
},
|
||||||
|
["^Enter"]: () => {
|
||||||
|
$goto("./new")
|
||||||
|
},
|
||||||
|
["Delete"]: component => {
|
||||||
|
// Don't show confirmation for the screen itself
|
||||||
|
if (component?._id === $selectedScreen.props._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
componentToDelete = component
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
},
|
||||||
|
["ArrowUp"]: () => {
|
||||||
|
store.actions.components.selectPrevious()
|
||||||
|
},
|
||||||
|
["ArrowDown"]: () => {
|
||||||
|
store.actions.components.selectNext()
|
||||||
|
},
|
||||||
|
["Escape"]: () => {
|
||||||
|
if (!$isActive("/new")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
$goto("./")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyAction = async (component, key, ctrlKey = false) => {
|
||||||
|
if (!component || !key) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Delete and backspace are the same
|
||||||
|
if (key === "Backspace") {
|
||||||
|
key = "Delete"
|
||||||
|
}
|
||||||
|
// Prefix key with a caret for ctrl modifier
|
||||||
|
if (ctrlKey) {
|
||||||
|
key = "^" + key
|
||||||
|
}
|
||||||
|
const handler = keyHandlers[key]
|
||||||
|
if (!handler) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return handler(component)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error handling key press")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Key events are always for the selected component
|
||||||
|
return handleKeyAction($selectedComponent, e.key, e.ctrlKey || e.metaKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComponentMenu = async e => {
|
||||||
|
// Menu events can be for any component
|
||||||
|
const { id, key, ctrlKey } = e.detail
|
||||||
|
const component = findComponent($selectedScreen.props, id)
|
||||||
|
return await handleKeyAction(component, key, ctrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("keydown", handleKeyPress)
|
||||||
|
document.addEventListener("component-menu", handleComponentMenu)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress)
|
||||||
|
document.removeEventListener("component-menu", handleComponentMenu)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
body={`Are you sure you want to delete "${componentToDelete?._instanceName}"?`}
|
||||||
|
okText="Delete Component"
|
||||||
|
onOk={() => store.actions.components.delete(componentToDelete)}
|
||||||
|
/>
|
|
@ -2,62 +2,15 @@
|
||||||
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, isActive } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { store, selectedScreen, selectedComponent } from "builderStore"
|
import { store, selectedScreen } 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, 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 ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { notifications, Button } from "@budibase/bbui"
|
import { notifications, Button } from "@budibase/bbui"
|
||||||
|
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||||
let scrollRef
|
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
const scrollTo = bounds => {
|
|
||||||
if (!bounds) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sidebarWidth = 259
|
|
||||||
const navItemHeight = 32
|
|
||||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
|
||||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
|
||||||
let newOffsets = {}
|
|
||||||
|
|
||||||
// Calculate left offset
|
|
||||||
const offsetX = bounds.left + bounds.width + scrollLeft - 36
|
|
||||||
if (offsetX > sidebarWidth) {
|
|
||||||
newOffsets.left = offsetX - sidebarWidth
|
|
||||||
} else {
|
|
||||||
newOffsets.left = 0
|
|
||||||
}
|
|
||||||
if (newOffsets.left === scrollLeft) {
|
|
||||||
delete newOffsets.left
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate top offset
|
|
||||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
|
||||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
|
||||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
|
||||||
} else if (offsetY < scrollTop + navItemHeight) {
|
|
||||||
newOffsets.top = offsetY - navItemHeight
|
|
||||||
} else {
|
|
||||||
delete newOffsets.top
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if offset is unchanged
|
|
||||||
if (newOffsets.left == null && newOffsets.top == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smoothly scroll to the offset
|
|
||||||
scrollRef.scroll({
|
|
||||||
...newOffsets,
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -67,95 +20,15 @@
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set scroll context so components can invoke scrolling when selected
|
|
||||||
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.ctrlKey || e.metaKey) {
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault()
|
|
||||||
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 === "d") {
|
|
||||||
e.preventDefault()
|
|
||||||
await store.actions.components.copy(component)
|
|
||||||
await store.actions.components.paste(component, "below")
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
e.preventDefault()
|
|
||||||
$goto("./new")
|
|
||||||
}
|
|
||||||
} else if (e.key === "Backspace" || e.key === "Delete") {
|
|
||||||
// Don't show confirmation for the screen itself
|
|
||||||
if (component._id === get(selectedScreen).props._id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
confirmDeleteDialog.show()
|
|
||||||
} 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 title="Components" showExpandIcon borderRight>
|
<Panel title="Components" showExpandIcon borderRight>
|
||||||
<div class="add-component">
|
<div class="add-component">
|
||||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-items-container" bind:this={scrollRef}>
|
<ComponentScrollWrapper>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li>
|
||||||
on:click={() => {
|
|
||||||
$store.selectedComponentId = $selectedScreen?.props._id
|
|
||||||
}}
|
|
||||||
id={`component-${$selectedScreen?.props._id}`}
|
|
||||||
>
|
|
||||||
<NavItem
|
<NavItem
|
||||||
text="Screen"
|
text="Screen"
|
||||||
indentLevel={0}
|
indentLevel={0}
|
||||||
|
@ -164,6 +37,10 @@
|
||||||
scrollable
|
scrollable
|
||||||
icon="WebPage"
|
icon="WebPage"
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
|
on:click={() => {
|
||||||
|
$store.selectedComponentId = $selectedScreen?.props._id
|
||||||
|
}}
|
||||||
|
id={`component-${$selectedScreen?.props._id}`}
|
||||||
>
|
>
|
||||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
@ -187,15 +64,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</ComponentScrollWrapper>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ConfirmDialog
|
<ComponentKeyHandler />
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
title="Confirm Deletion"
|
|
||||||
body={`Are you sure you want to delete "${$selectedComponent?._instanceName}"?`}
|
|
||||||
okText="Delete Component"
|
|
||||||
onOk={deleteComponent}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.add-component {
|
.add-component {
|
||||||
|
@ -205,12 +76,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.nav-items-container {
|
|
||||||
padding: var(--spacing-xl) 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import { dndStore } from "./dndStore"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let scrollRef
|
||||||
|
|
||||||
|
const scrollTo = bounds => {
|
||||||
|
if (!bounds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sidebarWidth = 259
|
||||||
|
const navItemHeight = 32
|
||||||
|
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||||
|
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||||
|
let newOffsets = {}
|
||||||
|
|
||||||
|
// Calculate left offset
|
||||||
|
const offsetX = bounds.left + bounds.width + scrollLeft - 36
|
||||||
|
if (offsetX > sidebarWidth) {
|
||||||
|
newOffsets.left = offsetX - sidebarWidth
|
||||||
|
} else {
|
||||||
|
newOffsets.left = 0
|
||||||
|
}
|
||||||
|
if (newOffsets.left === scrollLeft) {
|
||||||
|
delete newOffsets.left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate top offset
|
||||||
|
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||||
|
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||||
|
} else if (offsetY < scrollTop + navItemHeight) {
|
||||||
|
newOffsets.top = offsetY - navItemHeight
|
||||||
|
} else {
|
||||||
|
delete newOffsets.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if offset is unchanged
|
||||||
|
if (newOffsets.left == null && newOffsets.top == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly scroll to the offset
|
||||||
|
scrollRef.scroll({
|
||||||
|
...newOffsets,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set scroll context so components can invoke scrolling when selected
|
||||||
|
setContext("scroll", {
|
||||||
|
scrollTo,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDrop = async () => {
|
||||||
|
try {
|
||||||
|
await dndStore.actions.drop()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
notifications.error("Error saving component")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={scrollRef}
|
||||||
|
on:drop={onDrop}
|
||||||
|
ondragover="return false"
|
||||||
|
ondragenter="return false"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
padding: var(--spacing-xl) 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -68,7 +68,8 @@
|
||||||
closedNodes = closedNodes
|
closedNodes = closedNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async e => {
|
||||||
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
await dndStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Reference in New Issue