mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
6 changed files with 224 additions and 155 deletions
@ -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)} |
|||
/> |
|||
@ -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> |
|||
Loading…
Reference in new issue