Merge pull request #7568 from Budibase/cheeks-fixes

Component action fixes
This commit is contained in:
Andrew Kingston 2022-09-01 15:21:48 +01:00 committed by GitHub
commit b49872bea3
6 changed files with 224 additions and 155 deletions

View File

@ -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}

View File

@ -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>

View File

@ -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)}
/>

View File

@ -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;

View File

@ -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>

View File

@ -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) {