Convert multiple files to TS and update BBUI to support TS

This commit is contained in:
Andrew Kingston 2025-01-17 09:39:33 +00:00
parent 5acd6ab3d6
commit 050f1e7a5f
No known key found for this signature in database
15 changed files with 297 additions and 158 deletions

View File

@ -3,7 +3,7 @@
"description": "A UI solution used in the different Budibase projects.",
"version": "0.0.0",
"license": "MPL-2.0",
"svelte": "src/index.js",
"svelte": "src/index.ts",
"module": "dist/bbui.mjs",
"exports": {
".": {
@ -14,7 +14,8 @@
"./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js"
},
"scripts": {
"build": "vite build"
"build": "vite build",
"dev": "vite build --watch --mode=dev"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "1.4.0",

View File

@ -1,3 +1,17 @@
type ClickOutsideCallback = (event: MouseEvent) => void | undefined
interface ClickOutsideOpts {
callback?: ClickOutsideCallback
anchor?: HTMLElement
}
interface Handler {
id: number
element: HTMLElement
anchor: HTMLElement
callback?: ClickOutsideCallback
}
// These class names will never trigger a callback if clicked, no matter what
const ignoredClasses = [
".download-js-link",
@ -14,18 +28,20 @@ const conditionallyIgnoredClasses = [
".drawer-wrapper",
".spectrum-Popover",
]
let clickHandlers = []
let candidateTarget
let clickHandlers: Handler[] = []
let candidateTarget: HTMLElement | undefined
// Processes a "click outside" event and invokes callbacks if our source element
// is valid
const handleClick = event => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
// Ignore click if this is an ignored class
if (event.target.closest('[data-ignore-click-outside="true"]')) {
if (target.closest('[data-ignore-click-outside="true"]')) {
return
}
for (let className of ignoredClasses) {
if (event.target.closest(className)) {
if (target.closest(className)) {
return
}
}
@ -33,41 +49,41 @@ const handleClick = event => {
// Process handlers
clickHandlers.forEach(handler => {
// Check that the click isn't inside the target
if (handler.element.contains(event.target)) {
if (handler.element.contains(target)) {
return
}
// Ignore clicks for certain classes unless we're nested inside them
for (let className of conditionallyIgnoredClasses) {
const sourceInside = handler.anchor.closest(className) != null
const clickInside = event.target.closest(className) != null
const clickInside = target.closest(className) != null
if (clickInside && !sourceInside) {
return
}
}
handler.callback?.(event)
handler.callback?.(e)
})
}
// On mouse up we only trigger a "click outside" callback if we targetted the
// same element that we did on mouse down. This fixes all sorts of issues where
// we get annoying callbacks firing when we drag to select text.
const handleMouseUp = e => {
const handleMouseUp = (e: MouseEvent) => {
if (candidateTarget === e.target) {
handleClick(e)
}
candidateTarget = null
candidateTarget = undefined
}
// On mouse down we store which element was targetted for comparison later
const handleMouseDown = e => {
const handleMouseDown = (e: MouseEvent) => {
// Only handle the primary mouse button here.
// We handle context menu (right click) events in another handler.
if (e.button !== 0) {
return
}
candidateTarget = e.target
candidateTarget = e.target as HTMLElement
// Clear any previous listeners in case of multiple down events, and register
// a single mouse up listener
@ -82,7 +98,12 @@ document.addEventListener("contextmenu", handleClick)
/**
* Adds or updates a click handler
*/
const updateHandler = (id, element, anchor, callback) => {
const updateHandler = (
id: number,
element: HTMLElement,
anchor: HTMLElement,
callback: ClickOutsideCallback | undefined
) => {
let existingHandler = clickHandlers.find(x => x.id === id)
if (!existingHandler) {
clickHandlers.push({ id, element, anchor, callback })
@ -94,27 +115,52 @@ const updateHandler = (id, element, anchor, callback) => {
/**
* Removes a click handler
*/
const removeHandler = id => {
const removeHandler = (id: number) => {
clickHandlers = clickHandlers.filter(x => x.id !== id)
}
/**
* Svelte action to apply a click outside handler for a certain element
* Svelte action to apply a click outside handler for a certain element.
* opts.anchor is an optional param specifying the real root source of the
* component being observed. This is required for things like popovers, where
* the element using the clickoutside action is the popover, but the popover is
* rendered at the root of the DOM somewhere, whereas the popover anchor is the
* element we actually want to consider when determining the source component.
*/
export default (element, opts) => {
export default (
element: HTMLElement,
opts?: ClickOutsideOpts | ClickOutsideCallback
) => {
const id = Math.random()
const update = newOpts => {
const callback =
newOpts?.callback || (typeof newOpts === "function" ? newOpts : null)
const anchor = newOpts?.anchor || element
const isCallback = (
opts?: ClickOutsideOpts | ClickOutsideCallback
): opts is ClickOutsideCallback => {
return typeof opts === "function"
}
const isOpts = (
opts?: ClickOutsideOpts | ClickOutsideCallback
): opts is ClickOutsideOpts => {
return opts != null && typeof opts === "object"
}
const update = (newOpts?: ClickOutsideOpts | ClickOutsideCallback) => {
let callback: ClickOutsideCallback | undefined
let anchor = element
if (isCallback(newOpts)) {
callback = newOpts
} else if (isOpts(newOpts)) {
callback = newOpts.callback
if (newOpts.anchor) {
anchor = newOpts.anchor
}
}
updateHandler(id, element, anchor, callback)
}
update(opts)
return {
update,
destroy: () => removeHandler(id),

View File

@ -1,10 +1,4 @@
/**
* Valid alignment options are
* - left
* - right
* - left-outside
* - right-outside
**/
import { PopoverAlignment } from "../Popover/Popover.svelte"
// Strategies are defined as [Popover]To[Anchor].
// They can apply for both horizontal and vertical alignment.
@ -149,20 +143,29 @@ export default function positionDropdown(element, opts) {
}
// Determine X strategy
if (align === "right") {
if (align === PopoverAlignment.Right) {
applyXStrategy(Strategies.EndToEnd)
} else if (align === "right-outside" || align === "right-context-menu") {
} else if (
align === PopoverAlignment.RightOutside ||
align === PopoverAlignment.RightContextMenu
) {
applyXStrategy(Strategies.StartToEnd)
} else if (align === "left-outside" || align === "left-context-menu") {
} else if (
align === PopoverAlignment.LeftOutside ||
align === PopoverAlignment.LeftContextMenu
) {
applyXStrategy(Strategies.EndToStart)
} else if (align === "center") {
} else if (align === PopoverAlignment.Center) {
applyXStrategy(Strategies.MidPoint)
} else {
applyXStrategy(Strategies.StartToStart)
}
// Determine Y strategy
if (align === "right-outside" || align === "left-outside") {
if (
align === PopoverAlignment.RightOutside ||
align === PopoverAlignment.LeftOutside
) {
applyYStrategy(Strategies.MidPoint)
} else if (
align === "right-context-menu" ||

View File

@ -1,23 +1,23 @@
<script>
<script lang="ts">
import {
default as AbsTooltip,
TooltipPosition,
TooltipType,
} from "../Tooltip/AbsTooltip.svelte"
export let name = "Add"
export let hidden = false
export let size = "M"
export let hoverable = false
export let disabled = false
export let color
export let hoverColor
export let tooltip
export let name: string = "Add"
export let hidden: boolean = false
export let size: "XS" | "S" | "M" | "L" | "XL" = "M"
export let hoverable: boolean = false
export let disabled: boolean = false
export let color: string | undefined = undefined
export let hoverColor: string | undefined = undefined
export let tooltip: string | undefined = undefined
export let tooltipPosition = TooltipPosition.Bottom
export let tooltipType = TooltipType.Default
export let tooltipColor
export let tooltipWrap = true
export let newStyles = false
export let tooltipColor: string | undefined = undefined
export let tooltipWrap: boolean = true
export let newStyles: boolean = false
</script>
<AbsTooltip

View File

@ -1,4 +1,21 @@
<script>
<script context="module" lang="ts">
export enum PopoverAlignment {
Left = "left",
Right = "right",
LeftOutside = "left-outside",
RightOutside = "right-outside",
Center = "center",
RightContextMenu = "right-context-menu",
LeftContextMenu = "left-context-menu",
}
export interface PopoverAPI {
show: () => void
hide: () => void
}
</script>
<script lang="ts">
import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal"
import { createEventDispatcher, getContext, onDestroy } from "svelte"
@ -9,31 +26,35 @@
const dispatch = createEventDispatcher()
export let anchor
export let align = "right"
export let portalTarget
export let minWidth
export let maxWidth
export let maxHeight
export let open = false
export let useAnchorWidth = false
export let dismissible = true
export let offset = 4
export let customHeight
export let animate = true
export let customZindex
export let handlePostionUpdate
export let showPopover = true
export let clickOutsideOverride = false
export let resizable = true
export let wrap = false
export let anchor: HTMLElement | undefined = undefined
export let align: PopoverAlignment = PopoverAlignment.Right
export let portalTarget: string | undefined = undefined
export let minWidth: number | undefined = undefined
export let maxWidth: number | undefined = undefined
export let maxHeight: number | undefined = undefined
export let open: boolean = false
export let useAnchorWidth: boolean = false
export let dismissible: boolean = true
export let offset: number = 4
export let customHeight: string | undefined = undefined
export let animate: boolean = true
export let customZIndex: number | undefined = undefined
export let handlePositionUpdate: Function | undefined = undefined
export let showPopover: boolean = true
export let clickOutsideOverride: boolean = false
export let resizable: boolean = true
export let wrap: boolean = false
const animationDuration = 260
let timeout
let timeout: ReturnType<typeof setTimeout> | undefined
let blockPointerEvents = false
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
// Portal library lacks types, so we have to type this as any even though it's
// actually a string
$: target = (portalTarget ||
getContext(Context.PopoverRoot) ||
".spectrum") as any
$: {
// Disable pointer events for the initial part of the animation, because we
// fly from top to bottom and initially can be positioned under the cursor,
@ -113,7 +134,7 @@
minWidth,
useAnchorWidth,
offset,
customUpdate: handlePostionUpdate,
customUpdate: handlePositionUpdate,
resizable,
wrap,
}}
@ -123,11 +144,11 @@
}}
on:keydown={handleEscape}
class="spectrum-Popover is-open"
class:customZindex
class:customZIndex
class:hidden={!showPopover}
class:blockPointerEvents
role="presentation"
style="height: {customHeight}; --customZindex: {customZindex};"
style="height: {customHeight}; --customZIndex: {customZIndex};"
transition:fly|local={{
y: -20,
duration: animate ? animationDuration : 0,
@ -157,7 +178,7 @@
opacity: 0;
pointer-events: none;
}
.customZindex {
z-index: var(--customZindex) !important;
.customZIndex {
z-index: var(--customZIndex) !important;
}
</style>

View File

@ -1,3 +0,0 @@
declare module "./helpers" {
export const cloneDeep: <T>(obj: T) => T
}

View File

@ -45,7 +45,11 @@ export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
export { default as Icon } from "./Icon/Icon.svelte"
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
export { default as Popover } from "./Popover/Popover.svelte"
export {
default as Popover,
PopoverAlignment,
type PopoverAPI,
} from "./Popover/Popover.svelte"
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
export { default as Label } from "./Label/Label.svelte"

View File

@ -0,0 +1,7 @@
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
const config = {
preprocess: vitePreprocess(),
}
module.exports = config

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"allowJs": true,
"outDir": "./dist",
"lib": ["ESNext"],
"baseUrl": ".",
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
}

View File

@ -9,7 +9,7 @@ export default defineConfig(({ mode }) => {
build: {
sourcemap: !isProduction,
lib: {
entry: "src/index.js",
entry: "src/index.ts",
formats: ["es"],
},
},

View File

@ -1,14 +1,20 @@
<script>
import { Popover, Icon } from "@budibase/bbui"
<script lang="ts">
import {
Popover,
PopoverAlignment,
type PopoverAPI,
Icon,
} from "@budibase/bbui"
export let title
export let align = "left"
export let showPopover
export let width
export let title: string = ""
export let subtitle: string | undefined = undefined
export let align: PopoverAlignment = PopoverAlignment.Left
export let showPopover: boolean = true
export let width: number | undefined = undefined
let popover
let anchor
let open
let popover: PopoverAPI | undefined
let anchor: HTMLElement | undefined
let open: boolean = false
export const show = () => popover?.show()
export const hide = () => popover?.hide()
@ -30,20 +36,24 @@
{showPopover}
on:open
on:close
customZindex={100}
customZIndex={100}
>
<div class="detail-popover">
<div class="detail-popover__header">
<div class="detail-popover__title">
{title}
<Icon
name="Close"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
on:click={hide}
size="S"
/>
</div>
<Icon
name="Close"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectum-global-color-gray-900)"
on:click={hide}
/>
{#if subtitle}
<div class="detail-popover__subtitle">{subtitle}</div>
{/if}
</div>
<div class="detail-popover__body">
<slot />
@ -56,14 +66,18 @@
background-color: var(--spectrum-alias-background-color-primary);
}
.detail-popover__header {
display: flex;
flex-direction: column;
align-items: stretch;
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
padding: var(--spacing-l) var(--spacing-xl);
gap: var(--spacing-s);
}
.detail-popover__title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
padding: var(--spacing-l) var(--spacing-xl);
}
.detail-popover__title {
font-size: 16px;
font-weight: 600;
}

View File

@ -25,7 +25,7 @@
</div>
<Popover
customZindex={998}
customZIndex={998}
bind:this={formPopover}
align="center"
anchor={formPopoverAnchor}

View File

@ -96,8 +96,8 @@
maxWidth={300}
dismissible={false}
offset={12}
handlePostionUpdate={tourStep?.positionHandler}
customZindex={3}
handlePositionUpdate={tourStep?.positionHandler}
customZIndex={3}
>
<div class="tour-content">
<Layout noPadding gap="M">

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { AbsTooltip, Icon, TooltipPosition } from "@budibase/bbui"
import { Icon } from "@budibase/bbui"
export let label: string | undefined = undefined
export let value: any = undefined
@ -9,20 +9,22 @@
const Colors = {
Array: "var(--spectrum-global-color-gray-600)",
Object: "var(--spectrum-global-color-gray-600)",
Other: "var(--spectrum-global-color-indigo-600)",
Undefined: "var(--spectrum-global-color-gray-500)",
Null: "var(--spectrum-global-color-magenta-600)",
String: "var(--spectrum-global-color-orange-600)",
Number: "var(--spectrum-global-color-blue-600)",
True: "var(--spectrum-global-color-green-600)",
False: "var(--spectrum-global-color-red-600)",
Date: "var(--spectrum-global-color-green-600)",
Other: "var(--spectrum-global-color-blue-700)",
Undefined: "var(--spectrum-global-color-gray-600)",
Null: "var(--spectrum-global-color-yellow-700)",
String: "var(--spectrum-global-color-orange-700)",
Number: "var(--spectrum-global-color-purple-700)",
True: "var(--spectrum-global-color-celery-700)",
False: "var(--spectrum-global-color-red-700)",
Date: "var(--spectrum-global-color-green-700)",
}
let expanded = false
let valueExpanded = false
$: isArray = Array.isArray(value)
$: isObject = value?.toString?.() === "[object Object]"
$: primitive = !(isArray || isObject)
$: keys = getKeys(isArray, isObject, value)
$: expandable = keys.length > 0
$: displayValue = getDisplayValue(isArray, isObject, keys, value)
@ -96,13 +98,15 @@
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="binding-node">
{#if label != null}
<div class="binding-text">
<div class="binding-arrow">
<div class="binding-arrow" class:expanded>
{#if expandable}
<Icon
name={expanded ? "ChevronDown" : "ChevronRight"}
name="Play"
hoverable
color="var(--spectrum-global-color-gray-600)"
hoverColor="var(--spectrum-global-color-gray-900)"
@ -112,19 +116,21 @@
</div>
<div
class="binding-label"
class:primitive
class:expandable
on:click={() => (expanded = !expanded)}
>
{label}
</div>
<AbsTooltip
text={isArray || isObject ? null : displayValue}
position={TooltipPosition.Right}
<div
class="binding-value"
class:primitive
class:expanded={valueExpanded}
{style}
on:click={() => (valueExpanded = !valueExpanded)}
>
<div class="binding-value" class:expandable {style}>
{displayValue}
</div>
</AbsTooltip>
{displayValue}
</div>
</div>
{/if}
{#if expandable && (expanded || label == null)}
@ -150,24 +156,33 @@
overflow: hidden;
}
.binding-arrow {
margin-right: 2px;
margin: -3px 2px -2px 0;
flex: 0 0 18px;
transition: transform 130ms ease-out;
}
.binding-arrow :global(svg) {
width: 9px;
}
.binding-arrow.expanded {
transform: rotate(90deg);
}
.binding-text {
display: flex;
flex-direction: row;
align-items: center;
font-family: monospace;
font-size: 12px;
align-items: flex-start;
width: 100%;
}
.binding-children {
display: flex;
flex-direction: column;
gap: 8px;
/* border-left: 1px solid var(--spectrum-global-color-gray-400); */
/* margin-left: 20px; */
padding-left: 18px;
/*padding-left: 18px;*/
border-left: 1px solid var(--spectrum-global-color-gray-400);
margin-left: 20px;
padding-left: 3px;
}
.binding-children.root {
border-left: none;
@ -176,31 +191,40 @@
}
/* Size label and value according to type */
.binding-label,
.binding-value {
.binding-label {
flex: 0 1 auto;
margin-right: 8px;
transition: color 130ms ease-out;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.binding-label {
flex: 0 0 auto;
max-width: 50%;
margin-right: 8px;
transition: color 130ms ease-out;
}
.binding-label.expandable:hover {
cursor: pointer;
color: var(--spectrum-global-color-gray-900);
}
.binding-value {
flex: 0 1 auto;
}
.binding-label.expandable {
flex: 0 1 auto;
max-width: none;
}
.binding-value.expandable {
flex: 0 0 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: filter 130ms ease-out;
}
.binding-value.primitive:hover {
filter: brightness(1.25);
cursor: pointer;
}
.binding-value.expanded {
word-break: break-all;
white-space: wrap;
}
.binding-label.primitive {
flex: 0 0 auto;
max-width: 50%;
}
.binding-value.primitive {
flex: 0 1 auto;
}
/* Trim spans in the highlighted HTML */

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { ActionButton, Modal, ModalContent, Helpers } from "@budibase/bbui"
import { ActionButton, Helpers, PopoverAlignment } from "@budibase/bbui"
import {
previewStore,
selectedScreen,
@ -9,6 +9,7 @@
import { getBindableProperties } from "@/dataBinding"
import BindingNode from "./BindingExplorer/BindingNode.svelte"
import { processObjectSync } from "@budibase/string-templates"
import DetailPopover from "@/components/common/DetailPopover.svelte"
// Minimal typing for the real data binding structure, as none exists
type DataBinding = {
@ -17,18 +18,11 @@
readableBinding: string
}
let modal: any
$: previewContext = $previewStore.selectedComponentContext || {}
$: selectedComponentId = $componentStore.selectedComponentId
$: context = makeContext(previewContext, bindings)
$: bindings = getBindableProperties($selectedScreen, selectedComponentId)
const show = () => {
previewStore.requestComponentContext()
modal.show()
}
const makeContext = (
previewContext: Record<string, any>,
bindings: DataBinding[]
@ -77,15 +71,32 @@
}
</script>
<ActionButton on:click={show}>Bindings</ActionButton>
<Modal bind:this={modal}>
<ModalContent
title="Bindings"
showConfirmButton={false}
cancelText="Close"
size="M"
>
<DetailPopover
title="Data context"
subtitle="Showing all bindable data context available on the /employees screen."
align={PopoverAlignment.Right}
>
<svelte:fragment slot="anchor" let:open>
<ActionButton
icon="Code"
quiet
selected={open}
on:click={previewStore.requestComponentContext}
>
Data context
</ActionButton>
</svelte:fragment>
<div class="bindings">
<BindingNode value={context} />
</ModalContent>
</Modal>
</div>
</DetailPopover>
<style>
.bindings {
margin: calc(-1 * var(--spacing-xl));
padding: 20px 12px;
background: var(--spectrum-global-color-gray-50);
overflow-y: auto;
max-height: 600px;
}
</style>