Add double layer context menu for overflow views
This commit is contained in:
parent
40e7f58131
commit
d377186f0d
|
@ -115,6 +115,7 @@
|
|||
}
|
||||
.spectrum-ActionButton--quiet.is-selected {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.noPadding {
|
||||
padding: 0;
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<script>
|
||||
import { setContext } from "svelte"
|
||||
import { setContext, getContext } from "svelte"
|
||||
import Popover from "../Popover/Popover.svelte"
|
||||
import Menu from "../Menu/Menu.svelte"
|
||||
|
||||
export let disabled = false
|
||||
export let align = "left"
|
||||
export let portalTarget
|
||||
export let openOnHover = false
|
||||
export let animate
|
||||
export let offset
|
||||
|
||||
const actionMenuContext = getContext("actionMenu")
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
let timeout
|
||||
|
||||
// This is needed because display: contents is considered "invisible".
|
||||
// It should only ever be an action button, so should be fine.
|
||||
|
@ -16,11 +22,19 @@
|
|||
anchor = node.firstChild
|
||||
}
|
||||
|
||||
export const show = e => {
|
||||
cancelHide()
|
||||
dropdown.show()
|
||||
}
|
||||
|
||||
export const hide = () => {
|
||||
dropdown.hide()
|
||||
}
|
||||
export const show = () => {
|
||||
dropdown.show()
|
||||
|
||||
// Hides this menu and all parent menus
|
||||
const hideAll = () => {
|
||||
hide()
|
||||
actionMenuContext?.hide()
|
||||
}
|
||||
|
||||
const openMenu = event => {
|
||||
|
@ -30,12 +44,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
setContext("actionMenu", { show, hide })
|
||||
const queueHide = () => {
|
||||
timeout = setTimeout(hide, 10)
|
||||
}
|
||||
|
||||
const cancelHide = () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
setContext("actionMenu", { show, hide, hideAll })
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div use:getAnchor on:click={openMenu}>
|
||||
<div
|
||||
use:getAnchor
|
||||
on:click={openOnHover ? null : openMenu}
|
||||
on:mouseenter={openOnHover ? show : null}
|
||||
on:mouseleave={openOnHover ? queueHide : null}
|
||||
>
|
||||
<slot name="control" />
|
||||
</div>
|
||||
<Popover
|
||||
|
@ -43,9 +70,13 @@
|
|||
{anchor}
|
||||
{align}
|
||||
{portalTarget}
|
||||
{animate}
|
||||
{offset}
|
||||
resizable={false}
|
||||
on:open
|
||||
on:close
|
||||
on:mouseenter={openOnHover ? cancelHide : null}
|
||||
on:mouseleave={openOnHover ? queueHide : null}
|
||||
>
|
||||
<Menu>
|
||||
<slot />
|
||||
|
|
|
@ -151,9 +151,9 @@ export default function positionDropdown(element, opts) {
|
|||
// Determine X strategy
|
||||
if (align === "right") {
|
||||
applyXStrategy(Strategies.EndToEnd)
|
||||
} else if (align === "right-outside") {
|
||||
} else if (align === "right-outside" || align === "right-context-menu") {
|
||||
applyXStrategy(Strategies.StartToEnd)
|
||||
} else if (align === "left-outside") {
|
||||
} else if (align === "left-outside" || align === "left-context-menu") {
|
||||
applyXStrategy(Strategies.EndToStart)
|
||||
} else if (align === "center") {
|
||||
applyXStrategy(Strategies.MidPoint)
|
||||
|
@ -164,6 +164,12 @@ export default function positionDropdown(element, opts) {
|
|||
// Determine Y strategy
|
||||
if (align === "right-outside" || align === "left-outside") {
|
||||
applyYStrategy(Strategies.MidPoint)
|
||||
} else if (
|
||||
align === "right-context-menu" ||
|
||||
align === "left-context-menu"
|
||||
) {
|
||||
applyYStrategy(Strategies.StartToStart)
|
||||
styles.top -= 4 // Manual adjustment for action menu padding
|
||||
} else {
|
||||
applyYStrategy(Strategies.StartToEnd)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
const onClick = () => {
|
||||
if (actionMenu && !noClose) {
|
||||
actionMenu.hide()
|
||||
actionMenu.hideAll()
|
||||
}
|
||||
dispatch("click")
|
||||
}
|
||||
|
@ -47,8 +47,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel"><slot /></span>
|
||||
{#if keys?.length}
|
||||
{#if keys?.length || $$slots.right}
|
||||
<div class="keys">
|
||||
<slot name="right" />
|
||||
{#each keys as key}
|
||||
<div class="key">
|
||||
{#if key.startsWith("!")}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import Portal from "svelte-portal"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
import { createEventDispatcher, getContext, onDestroy } from "svelte"
|
||||
import positionDropdown from "../Actions/position_dropdown"
|
||||
import clickOutside from "../Actions/click_outside"
|
||||
import { fly } from "svelte/transition"
|
||||
|
@ -28,7 +28,21 @@
|
|||
export let resizable = true
|
||||
export let wrap = false
|
||||
|
||||
const animationDuration = 260
|
||||
|
||||
let timeout
|
||||
let blockPointerEvents = false
|
||||
|
||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||
$: {
|
||||
if (open && animate) {
|
||||
blockPointerEvents = true
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
blockPointerEvents = false
|
||||
}, animationDuration / 2)
|
||||
}
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
dispatch("open")
|
||||
|
@ -77,6 +91,10 @@
|
|||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
|
@ -104,9 +122,13 @@
|
|||
class="spectrum-Popover is-open"
|
||||
class:customZindex
|
||||
class:hidden={!showPopover}
|
||||
class:blockPointerEvents
|
||||
role="presentation"
|
||||
style="height: {customHeight}; --customZindex: {customZindex};"
|
||||
transition:fly|local={{ y: -20, duration: animate ? 260 : 0 }}
|
||||
transition:fly|local={{
|
||||
y: -20,
|
||||
duration: animate ? animationDuration : 0,
|
||||
}}
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
|
@ -122,6 +144,9 @@
|
|||
overflow: auto;
|
||||
transition: opacity 260ms ease-out;
|
||||
}
|
||||
.blockPointerEvents {
|
||||
pointer-events: none;
|
||||
}
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
contextMenuStore,
|
||||
} from "stores/builder"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import { Icon, Button, Popover, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import {
|
||||
Icon,
|
||||
Button,
|
||||
ActionButton,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
} from "@budibase/bbui"
|
||||
import { params, url } from "@roxi/routify"
|
||||
import EditViewModal from "./EditViewModal.svelte"
|
||||
import DeleteViewModal from "./DeleteViewModal.svelte"
|
||||
|
@ -19,12 +25,13 @@
|
|||
import { alphabetical } from "components/backend/TableNavigator/utils"
|
||||
import CreateViewModal from "./CreateViewModal.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { derived } from "svelte/store"
|
||||
|
||||
let viewContainer
|
||||
// View overflow
|
||||
let observer
|
||||
let viewContainer
|
||||
let viewVisibiltyMap = {}
|
||||
let overflowPopover
|
||||
let anchor
|
||||
let overflowMenu
|
||||
|
||||
// Editing table
|
||||
let createViewModal
|
||||
|
@ -46,8 +53,15 @@
|
|||
.filter(x => x.version === 2)
|
||||
.slice()
|
||||
.sort(alphabetical)
|
||||
$: setUpObserver(viewContainer, views)
|
||||
$: setUpObserver(views)
|
||||
$: overflowedViews = views.filter(view => !viewVisibiltyMap[view.id])
|
||||
$: viewHidden = viewVisibiltyMap[activeId] === false
|
||||
|
||||
const viewUrl = derived([url, params], ([$url, $params]) => viewId => {
|
||||
return $url(`../${$params.tableId}/${encodeURIComponent(viewId)}`)
|
||||
})
|
||||
|
||||
const tableUrl = derived(url, $url => tableId => $url(`../${tableId}`))
|
||||
|
||||
const openTableContextMenu = e => {
|
||||
if (!tableEditable) {
|
||||
|
@ -114,11 +128,24 @@
|
|||
)
|
||||
}
|
||||
|
||||
const setUpObserver = (viewContainer, views) => {
|
||||
if (!views.length || !viewContainer) {
|
||||
const editOverflowView = async view => {
|
||||
editableView = view
|
||||
await tick()
|
||||
editViewModal?.show()
|
||||
}
|
||||
|
||||
const deleteOverflowView = async view => {
|
||||
editableView = view
|
||||
await tick()
|
||||
deleteViewModal?.show()
|
||||
}
|
||||
|
||||
const setUpObserver = async views => {
|
||||
observer?.disconnect()
|
||||
if (!views.length) {
|
||||
return
|
||||
}
|
||||
await tick()
|
||||
observer = new IntersectionObserver(
|
||||
entries => {
|
||||
let updates = {}
|
||||
|
@ -136,9 +163,11 @@
|
|||
}
|
||||
)
|
||||
for (let child of viewContainer.children) {
|
||||
if (child.dataset.id) {
|
||||
observer.observe(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.disconnect()
|
||||
|
@ -152,7 +181,7 @@
|
|||
size="24"
|
||||
/>
|
||||
<a
|
||||
href={$url(`../${tableId}`)}
|
||||
href={$tableUrl(tableId)}
|
||||
class="nav-item"
|
||||
class:active={tableId === activeId}
|
||||
on:contextmenu={openTableContextMenu}
|
||||
|
@ -178,7 +207,7 @@
|
|||
{#each views as view (view.id)}
|
||||
{@const selectedBy = $userSelectedResourceMap[view.id]}
|
||||
<a
|
||||
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
|
||||
href={$viewUrl(view.id)}
|
||||
class="nav-item"
|
||||
class:active={view.id === activeId}
|
||||
class:hidden={!viewVisibiltyMap[view.id]}
|
||||
|
@ -209,27 +238,42 @@
|
|||
</span>
|
||||
{/if}
|
||||
{#if overflowedViews.length}
|
||||
<ActionMenu align="right">
|
||||
<ActionMenu align="right" bind:this={overflowMenu}>
|
||||
<div slot="control">
|
||||
<Icon
|
||||
name="ChevronDown"
|
||||
size="XL"
|
||||
hoverable
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
hoverColor="var(--spectrum-global-color-gray-900)"
|
||||
on:click={overflowPopover?.show}
|
||||
/>
|
||||
<ActionButton icon="ChevronDown" quiet selected={viewHidden}>
|
||||
{overflowedViews.length} more
|
||||
</ActionButton>
|
||||
</div>
|
||||
{#each overflowedViews as view}
|
||||
<ActionMenu
|
||||
align="left-context-menu"
|
||||
openOnHover
|
||||
animate={false}
|
||||
offset={-4}
|
||||
>
|
||||
<div slot="control">
|
||||
<a
|
||||
href={$viewUrl(view.id)}
|
||||
class="nav-overflow-item"
|
||||
class:active={view.id === activeId}
|
||||
href={$url(`../${tableId}/${encodeURIComponent(view.id)}`)}
|
||||
on:click={overflowMenu?.hide}
|
||||
>
|
||||
<MenuItem>
|
||||
<MenuItem icon={viewHidden ? "Checkmark" : null}>
|
||||
{view.name}
|
||||
<Icon slot="right" name="ChevronRight" />
|
||||
</MenuItem>
|
||||
</a>
|
||||
</div>
|
||||
<a href={$viewUrl(view.id)}>
|
||||
<MenuItem icon="Checkmark">Select</MenuItem>
|
||||
</a>
|
||||
<MenuItem icon="Edit" on:click={() => editOverflowView(view)}>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={() => deleteOverflowView(view)}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
{/each}
|
||||
</ActionMenu>
|
||||
{/if}
|
||||
|
@ -266,22 +310,23 @@
|
|||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-xl);
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
.nav__views {
|
||||
width: 0;
|
||||
flex: 1 1 auto;
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
/* Table and view items */
|
||||
.nav-item {
|
||||
padding: 6px 8px;
|
||||
padding: 0 8px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -309,4 +354,9 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* OVerflow items */
|
||||
.nav-overflow-item:not(.active) :global(> .spectrum-Menu-item > .icon) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue