1
0
Fork 0
mirror of synced 2024-10-02 10:08:09 +13:00

Add modal drawer support with transitions

This commit is contained in:
Andrew Kingston 2024-02-24 13:26:34 +00:00
parent 7484f087bc
commit 395942d8b6
4 changed files with 134 additions and 29 deletions

View file

@ -1,7 +1,52 @@
<script context="module">
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
// Observe this class name if possible in order to know how to size the
// drawer. If this doesn't exist we'll use a fixed size.
const drawerContainer = "drawer-container"
// Context level stores to keep drawers in sync
const openDrawers = writable([])
const modal = writable(false)
const drawerLeft = writable(null)
const drawerWidth = writable(null)
// Resize observer to keep track of size changes
let observer
// Starts observing the target node to watching to size changes.
// Invoked when the first drawer of a chain is rendered.
const observe = () => {
const target = document.getElementsByClassName(drawerContainer)[0]
if (observer || !target) {
return
}
observer = new ResizeObserver(entries => {
if (!entries?.[0]) {
return
}
const bounds = entries[0].target.getBoundingClientRect()
drawerLeft.set(bounds.left)
drawerWidth.set(bounds.width)
})
observer.observe(target)
// Manually measure once to ensure that we have dimensions for the initial
// paint
const bounds = target.getBoundingClientRect()
drawerLeft.set(bounds.left)
drawerWidth.set(bounds.width)
}
// Stops observing the target node.
// Invoked when the last drawer of a chain is removed.
const unobserve = () => {
if (get(openDrawers).length) {
return
}
observer.disconnect()
observer = null
}
</script>
<script>
@ -9,21 +54,45 @@
import Button from "../Button/Button.svelte"
import { setContext, createEventDispatcher, onDestroy } from "svelte"
import { generate } from "shortid"
import { fade } from "svelte/transition"
export let title
export let headless = false
const dispatch = createEventDispatcher()
const spacing = 10
let visible = false
let drawerId = generate()
$: depth = $openDrawers.length - $openDrawers.indexOf(drawerId) - 1
$: style = getStyle(depth, $drawerLeft, $drawerWidth, $modal)
const getStyle = (depth, left, width, modal) => {
let style = `
--scale-factor: ${getScaleFactor(depth)};
--spacing: ${spacing}px;
`
// Most modal styles are handled by class names
if (modal) {
return style
}
// Normal drawers need a few additional styles
left = left ? `${left + width / 2}px` : "20vw"
width = width ? `${width - 2 * spacing}px` : "60vw"
return `
${style}
left: ${left};
width: ${width};
`
}
export function show() {
if (visible) {
return
}
observe()
visible = true
dispatch("drawerShow", drawerId)
openDrawers.update(state => [...state, drawerId])
@ -36,12 +105,16 @@
visible = false
dispatch("drawerHide", drawerId)
openDrawers.update(state => state.filter(id => id !== drawerId))
if (!$openDrawers.length) {
modal.set(false)
}
unobserve()
}
setContext("drawer-actions", {
setContext("drawer", {
hide,
show,
headless,
modal,
})
const easeInOutQuad = x => {
@ -52,10 +125,14 @@
// transition has a horrible overshoot
const slide = () => {
return {
duration: 360,
duration: 260,
css: t => {
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
return `transform: translateY(${translation}%);`
const f = easeInOutQuad(t)
const yOffset = (1 - f) * 200
return `
transform: translateX(-50%) translateY(calc(${yOffset}px + 50% - 1200px * (1 - var(--scale-factor))));
opacity:${f};
`
},
}
}
@ -75,13 +152,16 @@
</script>
{#if visible}
<Portal target=".drawer-container">
<Portal target=".modal-container">
{#if $modal}
<div transition:fade={{ duration: 260 }} class="underlay" />
{/if}
<div
class="drawer"
class:headless
class:modal={$modal}
transition:slide|local
style="--scale-factor:{getScaleFactor(depth)}"
class:stacked={depth > 0}
{style}
>
{#if !headless}
<header>
@ -93,41 +173,55 @@
</header>
{/if}
<slot name="body" />
{#if !$modal && depth > 0}
<div class="overlay" transition:fade|local={{ duration: 260 }} />
{/if}
</div>
</Portal>
{/if}
<style>
.drawer {
--drawer-spacing: 10px;
position: absolute;
left: var(--drawer-spacing);
bottom: var(--drawer-spacing);
transform: translateY(calc(-210% * (1 - var(--scale-factor))))
scale(var(--scale-factor));
width: calc(100% - 2 * var(--drawer-spacing));
transform: translateX(-50%) scale(var(--scale-factor))
translateY(calc(50% - 800px * (1 - var(--scale-factor))));
background: var(--background);
border: var(--border-light);
z-index: 3;
border-radius: 8px;
overflow: hidden;
box-sizing: border-box;
transition: transform 360ms ease-out;
transition: transform 260ms ease-out, bottom 260ms ease-out,
left 260ms ease-out, width 260ms ease-out, height 260ms ease-out;
height: 420px;
bottom: calc(var(--spacing) + 210px);
max-width: calc(100vw - 200px);
max-height: calc(100vh - 200px);
}
.drawer::after {
content: "";
position: absolute;
.drawer.modal {
left: 50vw;
bottom: 50vh;
width: 1600px;
height: 800px;
}
.overlay,
.underlay {
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--background);
pointer-events: none;
transition: opacity 360ms ease-out;
opacity: calc(10 * (1 - var(--scale-factor)));
z-index: 3;
opacity: 0.5;
}
.drawer.stacked::after {
pointer-events: all;
.overlay {
position: absolute;
background: var(--background);
}
.underlay {
position: fixed;
background: var(--modal-background, rgba(0, 0, 0, 0.75));
}
header {
@ -138,7 +232,6 @@
padding: var(--spacing-m) var(--spacing-xl);
gap: var(--spacing-xl);
}
.text {
display: flex;
flex-direction: column;
@ -146,7 +239,6 @@
align-items: flex-start;
gap: var(--spacing-xs);
}
.buttons {
display: flex;
flex-direction: row;

View file

@ -17,7 +17,7 @@
<style>
.drawer-contents {
height: 400px;
height: 100%;
overflow-y: auto;
}
.container {

View file

@ -1,6 +1,6 @@
<script>
import { DrawerContent, ActionButton, Icon } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte"
import { createEventDispatcher, getContext, onMount } from "svelte"
import {
isValid,
decodeJSBinding,
@ -37,6 +37,7 @@
export let context = null
export let autofocusEditor = false
const drawerContext = getContext("drawer")
const Modes = {
Text: "Text",
JavaScript: "JavaScript",
@ -49,14 +50,15 @@
let initialValueJS = value?.startsWith?.("{{ js ")
let mode = initialValueJS ? Modes.JavaScript : Modes.Text
let sidePanel = null
let getCaretPosition
let insertAtPos
let jsValue = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value
let targetMode = null
let expressionResult
let drawerIsModal
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
$: sideTabs = [SidePanels.Evaluation, SidePanels.Bindings]
$: enrichedBindings = enrichBindings(bindings, context)
@ -187,6 +189,16 @@
<Icon name={tab} size="S" />
</ActionButton>
{/each}
{#if drawerContext}
<ActionButton
size="M"
quiet
selected={drawerIsModal}
on:click={() => drawerContext.modal.set(!drawerIsModal)}
>
<Icon name={drawerIsModal ? "Minimize" : "Maximize"} size="S" />
</ActionButton>
{/if}
</div>
</div>
<div class="editor">

View file

@ -35,6 +35,7 @@
align-items: stretch;
padding: 9px var(--spacing-m);
position: relative;
transition: width 360ms ease-out;
}
.drawer-container {
position: absolute;