Tidy up component list panel monolith
This commit is contained in:
parent
d268554ac4
commit
7dc29140ca
|
@ -16,6 +16,7 @@
|
|||
export let scrollable = false
|
||||
export let highlighted = false
|
||||
export let rightAlignIcon = false
|
||||
export let id
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -58,6 +59,7 @@
|
|||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
{id}
|
||||
>
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
|
|
|
@ -1,23 +1,13 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
export let componentInstance
|
||||
|
||||
let confirmDialog
|
||||
const eject = () => {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "e", ctrlKey: true })
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ActionButton secondary on:click={confirmDialog.show}>
|
||||
Eject block
|
||||
</ActionButton>
|
||||
<ActionButton secondary on:click={eject}>Eject block</ActionButton>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDialog}
|
||||
title="Eject block"
|
||||
body="Ejecting a block breaks it down into multiple components. Are you sure you want to eject this block?"
|
||||
onOk={() =>
|
||||
store.actions.components.requestEjectBlock(componentInstance?._id)}
|
||||
okText="Eject block"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
<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 confirmEjectDialog
|
||||
let componentToDelete
|
||||
let componentToEject
|
||||
|
||||
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")
|
||||
},
|
||||
["^e"]: component => {
|
||||
componentToEject = component
|
||||
confirmEjectDialog.show()
|
||||
},
|
||||
["^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)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmEjectDialog}
|
||||
title="Eject block"
|
||||
body={`Ejecting a block breaks it down into multiple components. Are you sure you want to eject "${componentToEject?._instanceName}"?`}
|
||||
onOk={() => store.actions.components.requestEjectBlock(componentToEject?._id)}
|
||||
okText="Eject block"
|
||||
/>
|
|
@ -2,119 +2,15 @@
|
|||
import Panel from "components/design/Panel.svelte"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import { dndStore } from "./dndStore.js"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
import { store, selectedScreen, selectedComponent } from "builderStore"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||
import { setContext, onMount } from "svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { DropPosition } from "./dndStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { notifications, Button } from "@budibase/bbui"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
|
||||
let scrollRef
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
let componentToDelete
|
||||
let componentToEject
|
||||
|
||||
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")
|
||||
},
|
||||
["^e"]: component => {
|
||||
componentToEject = component
|
||||
confirmEjectDialog.show()
|
||||
},
|
||||
["^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 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,
|
||||
})
|
||||
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
|
||||
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
|
||||
|
||||
const onDrop = async () => {
|
||||
try {
|
||||
|
@ -124,74 +20,15 @@
|
|||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
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.log(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>
|
||||
|
||||
<Panel title="Components" showExpandIcon borderRight>
|
||||
<div class="add-component">
|
||||
<Button on:click={() => $goto("./new")} cta>Add component</Button>
|
||||
</div>
|
||||
<div class="nav-items-container" bind:this={scrollRef}>
|
||||
<ComponentScrollWrapper>
|
||||
<ul>
|
||||
<li
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = $selectedScreen?.props._id
|
||||
}}
|
||||
id={`component-${$selectedScreen?.props._id}`}
|
||||
>
|
||||
<li>
|
||||
<NavItem
|
||||
text="Screen"
|
||||
indentLevel={0}
|
||||
|
@ -200,6 +37,10 @@
|
|||
scrollable
|
||||
icon="WebPage"
|
||||
on:drop={onDrop}
|
||||
on:click={() => {
|
||||
$store.selectedComponentId = $selectedScreen?.props._id
|
||||
}}
|
||||
id={`component-${$selectedScreen?.props._id}`}
|
||||
>
|
||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||
</NavItem>
|
||||
|
@ -223,22 +64,9 @@
|
|||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ComponentScrollWrapper>
|
||||
</Panel>
|
||||
<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)}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
bind:this={confirmEjectDialog}
|
||||
title="Eject block"
|
||||
body={`Ejecting a block breaks it down into multiple components. Are you sure you want to eject "${componentToEject?._instanceName}"?`}
|
||||
onOk={() => store.actions.components.requestEjectBlock(componentToEject?._id)}
|
||||
okText="Eject block"
|
||||
/>
|
||||
<ComponentKeyHandler />
|
||||
|
||||
<style>
|
||||
.add-component {
|
||||
|
@ -248,12 +76,6 @@
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.nav-items-container {
|
||||
padding: var(--spacing-xl) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { setContext } from "svelte"
|
||||
|
||||
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,
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={scrollRef}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: var(--spacing-xl) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
|
@ -126,7 +126,7 @@
|
|||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if idx === 0 && componentDefinition?.block}
|
||||
<EjectBlockButton {componentInstance} />
|
||||
<EjectBlockButton />
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{/each}
|
||||
|
|
Loading…
Reference in New Issue