Add modal drawer support with transitions
This commit is contained in:
parent
7484f087bc
commit
395942d8b6
|
@ -1,7 +1,52 @@
|
||||||
<script context="module">
|
<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 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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -9,21 +54,45 @@
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
import { setContext, createEventDispatcher, onDestroy } from "svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let headless = false
|
export let headless = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
const spacing = 10
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
let drawerId = generate()
|
let drawerId = generate()
|
||||||
|
|
||||||
$: depth = $openDrawers.length - $openDrawers.indexOf(drawerId) - 1
|
$: 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() {
|
export function show() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
observe()
|
||||||
visible = true
|
visible = true
|
||||||
dispatch("drawerShow", drawerId)
|
dispatch("drawerShow", drawerId)
|
||||||
openDrawers.update(state => [...state, drawerId])
|
openDrawers.update(state => [...state, drawerId])
|
||||||
|
@ -36,12 +105,16 @@
|
||||||
visible = false
|
visible = false
|
||||||
dispatch("drawerHide", drawerId)
|
dispatch("drawerHide", drawerId)
|
||||||
openDrawers.update(state => state.filter(id => id !== drawerId))
|
openDrawers.update(state => state.filter(id => id !== drawerId))
|
||||||
|
if (!$openDrawers.length) {
|
||||||
|
modal.set(false)
|
||||||
|
}
|
||||||
|
unobserve()
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("drawer-actions", {
|
setContext("drawer", {
|
||||||
hide,
|
hide,
|
||||||
show,
|
show,
|
||||||
headless,
|
modal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const easeInOutQuad = x => {
|
const easeInOutQuad = x => {
|
||||||
|
@ -52,10 +125,14 @@
|
||||||
// transition has a horrible overshoot
|
// transition has a horrible overshoot
|
||||||
const slide = () => {
|
const slide = () => {
|
||||||
return {
|
return {
|
||||||
duration: 360,
|
duration: 260,
|
||||||
css: t => {
|
css: t => {
|
||||||
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
|
const f = easeInOutQuad(t)
|
||||||
return `transform: translateY(${translation}%);`
|
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>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<Portal target=".drawer-container">
|
<Portal target=".modal-container">
|
||||||
|
{#if $modal}
|
||||||
|
<div transition:fade={{ duration: 260 }} class="underlay" />
|
||||||
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="drawer"
|
class="drawer"
|
||||||
class:headless
|
class:headless
|
||||||
|
class:modal={$modal}
|
||||||
transition:slide|local
|
transition:slide|local
|
||||||
style="--scale-factor:{getScaleFactor(depth)}"
|
{style}
|
||||||
class:stacked={depth > 0}
|
|
||||||
>
|
>
|
||||||
{#if !headless}
|
{#if !headless}
|
||||||
<header>
|
<header>
|
||||||
|
@ -93,41 +173,55 @@
|
||||||
</header>
|
</header>
|
||||||
{/if}
|
{/if}
|
||||||
<slot name="body" />
|
<slot name="body" />
|
||||||
|
{#if !$modal && depth > 0}
|
||||||
|
<div class="overlay" transition:fade|local={{ duration: 260 }} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.drawer {
|
.drawer {
|
||||||
--drawer-spacing: 10px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--drawer-spacing);
|
transform: translateX(-50%) scale(var(--scale-factor))
|
||||||
bottom: var(--drawer-spacing);
|
translateY(calc(50% - 800px * (1 - var(--scale-factor))));
|
||||||
transform: translateY(calc(-210% * (1 - var(--scale-factor))))
|
|
||||||
scale(var(--scale-factor));
|
|
||||||
width: calc(100% - 2 * var(--drawer-spacing));
|
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border: var(--border-light);
|
border: var(--border-light);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
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 {
|
.drawer.modal {
|
||||||
content: "";
|
left: 50vw;
|
||||||
position: absolute;
|
bottom: 50vh;
|
||||||
|
width: 1600px;
|
||||||
|
height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay,
|
||||||
|
.underlay {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--background);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 360ms ease-out;
|
transition: opacity 360ms ease-out;
|
||||||
opacity: calc(10 * (1 - var(--scale-factor)));
|
z-index: 3;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.drawer.stacked::after {
|
.overlay {
|
||||||
pointer-events: all;
|
position: absolute;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--modal-background, rgba(0, 0, 0, 0.75));
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
@ -138,7 +232,6 @@
|
||||||
padding: var(--spacing-m) var(--spacing-xl);
|
padding: var(--spacing-m) var(--spacing-xl);
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -146,7 +239,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.drawer-contents {
|
.drawer-contents {
|
||||||
height: 400px;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { DrawerContent, ActionButton, Icon } from "@budibase/bbui"
|
import { DrawerContent, ActionButton, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import {
|
import {
|
||||||
isValid,
|
isValid,
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
export let context = null
|
export let context = null
|
||||||
export let autofocusEditor = false
|
export let autofocusEditor = false
|
||||||
|
|
||||||
|
const drawerContext = getContext("drawer")
|
||||||
const Modes = {
|
const Modes = {
|
||||||
Text: "Text",
|
Text: "Text",
|
||||||
JavaScript: "JavaScript",
|
JavaScript: "JavaScript",
|
||||||
|
@ -49,14 +50,15 @@
|
||||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||||
let mode = initialValueJS ? Modes.JavaScript : Modes.Text
|
let mode = initialValueJS ? Modes.JavaScript : Modes.Text
|
||||||
let sidePanel = null
|
let sidePanel = null
|
||||||
|
|
||||||
let getCaretPosition
|
let getCaretPosition
|
||||||
let insertAtPos
|
let insertAtPos
|
||||||
let jsValue = initialValueJS ? value : null
|
let jsValue = initialValueJS ? value : null
|
||||||
let hbsValue = initialValueJS ? null : value
|
let hbsValue = initialValueJS ? null : value
|
||||||
let targetMode = null
|
let targetMode = null
|
||||||
let expressionResult
|
let expressionResult
|
||||||
|
let drawerIsModal
|
||||||
|
|
||||||
|
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
|
||||||
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
||||||
$: sideTabs = [SidePanels.Evaluation, SidePanels.Bindings]
|
$: sideTabs = [SidePanels.Evaluation, SidePanels.Bindings]
|
||||||
$: enrichedBindings = enrichBindings(bindings, context)
|
$: enrichedBindings = enrichBindings(bindings, context)
|
||||||
|
@ -187,6 +189,16 @@
|
||||||
<Icon name={tab} size="S" />
|
<Icon name={tab} size="S" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/each}
|
{/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>
|
</div>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 9px var(--spacing-m);
|
padding: 9px var(--spacing-m);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: width 360ms ease-out;
|
||||||
}
|
}
|
||||||
.drawer-container {
|
.drawer-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
Loading…
Reference in New Issue