Add fade screen transition and fix navigation component casing
This commit is contained in:
parent
c91051dbc4
commit
9c4b9a2a25
|
@ -479,7 +479,7 @@ export const getFrontendStore = () => {
|
||||||
// Try to extract a nav component from the master screen
|
// Try to extract a nav component from the master screen
|
||||||
const nav = findChildComponentType(
|
const nav = findChildComponentType(
|
||||||
state.pages.main,
|
state.pages.main,
|
||||||
"@budibase/standard-components/Navigation"
|
"@budibase/standard-components/navigation"
|
||||||
)
|
)
|
||||||
if (nav) {
|
if (nav) {
|
||||||
let newLink
|
let newLink
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/old/state/eventHandlers"
|
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
const eventTypeKey = "##eventHandlerType"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@
|
||||||
$: actions = event || []
|
$: actions = event || []
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
||||||
.component
|
|
||||||
|
|
||||||
const updateEventHandler = (updatedHandler, index) => {
|
const updateEventHandler = (updatedHandler, index) => {
|
||||||
actions[index] = updatedHandler
|
actions[index] = updatedHandler
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
const addAction = actionType => () => {
|
const addAction = actionType => () => {
|
||||||
const newAction = {
|
const newAction = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
[eventTypeKey]: actionType.name,
|
||||||
}
|
}
|
||||||
actions.push(newAction)
|
actions.push(newAction)
|
||||||
selectedAction = newAction
|
selectedAction = newAction
|
||||||
|
@ -79,7 +78,7 @@
|
||||||
{#each actions as action, index}
|
{#each actions as action, index}
|
||||||
<div class="action-container">
|
<div class="action-container">
|
||||||
<div class="action-header" on:click={selectAction(action)}>
|
<div class="action-header" on:click={selectAction(action)}>
|
||||||
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
|
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
|
||||||
<div class="row-expander" class:rotate={action !== selectedAction}>
|
<div class="row-expander" class:rotate={action !== selectedAction}>
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1175,7 +1175,7 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nav Bar",
|
name: "Nav Bar",
|
||||||
_component: "@budibase/standard-components/Navigation",
|
_component: "@budibase/standard-components/navigation",
|
||||||
description:
|
description:
|
||||||
"A component for handling the navigation within your app.",
|
"A component for handling the navigation within your app.",
|
||||||
icon: "ri-navigation-line",
|
icon: "ri-navigation-line",
|
||||||
|
|
|
@ -30,3 +30,9 @@
|
||||||
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
import { screenStore, routeStore } from "../store"
|
import { screenStore, routeStore } from "../store"
|
||||||
import Component from "./Component.svelte"
|
import Component from "./Component.svelte"
|
||||||
|
|
||||||
|
@ -17,5 +18,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each screens as screen (screen._id)}
|
{#each screens as screen (screen._id)}
|
||||||
|
<div in:fade>
|
||||||
<Component definition={screen} />
|
<Component definition={screen} />
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { attachChildren } from "./render/attachChildren"
|
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
|
||||||
import { screenRouter } from "./render/screenRouter"
|
|
||||||
import { createStateManager } from "./state/stateManager"
|
|
||||||
import { getAppId } from "@budibase/component-sdk"
|
|
||||||
|
|
||||||
export const createApp = ({
|
|
||||||
componentLibraries,
|
|
||||||
frontendDefinition,
|
|
||||||
window,
|
|
||||||
}) => {
|
|
||||||
let routeTo
|
|
||||||
let currentUrl
|
|
||||||
let screenStateManager
|
|
||||||
|
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
|
||||||
const onScreenSelected = (screen, url) => {
|
|
||||||
const stateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered: () => {},
|
|
||||||
routeTo,
|
|
||||||
})
|
|
||||||
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
|
||||||
screenSlotNode.props._children = [screen.props]
|
|
||||||
const initialiseChildParams = getAttachChildrenParams(screenSlotNode)
|
|
||||||
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
if (screenStateManager) screenStateManager.destroy()
|
|
||||||
screenStateManager = stateManager
|
|
||||||
currentUrl = url
|
|
||||||
}
|
|
||||||
|
|
||||||
routeTo = screenRouter({
|
|
||||||
screens: frontendDefinition.screens,
|
|
||||||
onScreenSelected,
|
|
||||||
window,
|
|
||||||
})
|
|
||||||
const fallbackPath = window.location.pathname.replace(getAppId(), "")
|
|
||||||
routeTo(currentUrl || fallbackPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachChildrenParams = stateManager => {
|
|
||||||
const getInitialiseParams = treeNode => ({
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState: stateManager.setup,
|
|
||||||
})
|
|
||||||
|
|
||||||
return getInitialiseParams
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootTreeNode
|
|
||||||
const pageStateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
// seems weird, but the routeTo variable may not be available at this point
|
|
||||||
routeTo: url => routeTo(url),
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialisePage = (page, target, urlPath) => {
|
|
||||||
currentUrl = urlPath
|
|
||||||
|
|
||||||
rootTreeNode = createTreeNode()
|
|
||||||
rootTreeNode.props = {
|
|
||||||
_children: [page.props],
|
|
||||||
}
|
|
||||||
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
|
||||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
|
||||||
|
|
||||||
attachChildren(initChildParams)(target, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return rootTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialisePage,
|
|
||||||
screenStore: () => screenStateManager.store,
|
|
||||||
pageStore: () => pageStateManager.store,
|
|
||||||
routeTo: () => routeTo,
|
|
||||||
rootNode: () => rootTreeNode,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { createApp } from "./createApp"
|
|
||||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
|
||||||
import { getAppId } from "@budibase/component-sdk"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create a web application from static budibase definition files.
|
|
||||||
* @param {object} opts - configuration options for budibase client libary
|
|
||||||
*/
|
|
||||||
export const loadBudibase = async opts => {
|
|
||||||
const _window = (opts && opts.window) || window
|
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
|
||||||
|
|
||||||
const user = {}
|
|
||||||
|
|
||||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
|
||||||
|
|
||||||
const libraries = frontendDefinition.libraries || []
|
|
||||||
|
|
||||||
for (let library of libraries) {
|
|
||||||
// fetch the JavaScript for the component libraries from the server
|
|
||||||
componentLibraryModules[library] = await import(
|
|
||||||
`/componentlibrary?library=${encodeURI(library)}&appId=${appId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentLibraryModules[builtinLibName] = builtins(_window)
|
|
||||||
|
|
||||||
const {
|
|
||||||
initialisePage,
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
} = createApp({
|
|
||||||
componentLibraries: componentLibraryModules,
|
|
||||||
frontendDefinition,
|
|
||||||
user,
|
|
||||||
window: _window,
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = _window.location
|
|
||||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
|
||||||
: ""
|
|
||||||
|
|
||||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
|
||||||
|
|
||||||
return {
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window) {
|
|
||||||
window.loadBudibase = loadBudibase
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { prepareRenderComponent } from "./prepareRenderComponent"
|
|
||||||
import { isScreenSlot } from "./builtinComponents"
|
|
||||||
import deepEqual from "deep-equal"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
|
||||||
const {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
} = initialiseOpts
|
|
||||||
|
|
||||||
const anchor = options && options.anchor ? options.anchor : null
|
|
||||||
const force = options ? options.force : false
|
|
||||||
const hydrate = options ? options.hydrate : true
|
|
||||||
const context = options && options.context
|
|
||||||
|
|
||||||
if (!force && treeNode.children.length > 0) return treeNode.children
|
|
||||||
|
|
||||||
for (let childNode of treeNode.children) {
|
|
||||||
childNode.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!htmlElement) return
|
|
||||||
|
|
||||||
if (hydrate) {
|
|
||||||
while (htmlElement.firstChild) {
|
|
||||||
htmlElement.removeChild(htmlElement.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextStoreKeys = []
|
|
||||||
|
|
||||||
// create new context if supplied
|
|
||||||
if (context) {
|
|
||||||
let childIndex = 0
|
|
||||||
// if context is an array, map to new structure
|
|
||||||
const contextArray = Array.isArray(context) ? context : [context]
|
|
||||||
for (let ctx of contextArray) {
|
|
||||||
const key = appStore.create(
|
|
||||||
ctx,
|
|
||||||
treeNode.props._id,
|
|
||||||
childIndex,
|
|
||||||
treeNode.contextStoreKey
|
|
||||||
)
|
|
||||||
contextStoreKeys.push(key)
|
|
||||||
childIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNodes = []
|
|
||||||
|
|
||||||
const createChildNodes = contextStoreKey => {
|
|
||||||
for (let childProps of treeNode.props._children) {
|
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
|
||||||
|
|
||||||
if (!componentName || !libName) return
|
|
||||||
|
|
||||||
const ComponentConstructor = componentLibraries[libName][componentName]
|
|
||||||
|
|
||||||
const childNode = prepareRenderComponent({
|
|
||||||
props: childProps,
|
|
||||||
parentNode: treeNode,
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
// in same context as parent, unless a new one was supplied
|
|
||||||
contextStoreKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
childNodes.push(childNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context) {
|
|
||||||
// if new context(s) is supplied, then create nodes
|
|
||||||
// with keys to new context stores
|
|
||||||
for (let contextStoreKey of contextStoreKeys) {
|
|
||||||
createChildNodes(contextStoreKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise, use same context store as parent
|
|
||||||
// which maybe undefined (therfor using the root state)
|
|
||||||
createChildNodes(treeNode.contextStoreKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if everything is equal, then don't re-render
|
|
||||||
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
|
|
||||||
|
|
||||||
for (let node of childNodes) {
|
|
||||||
const initialProps = setupState(node)
|
|
||||||
node.render(initialProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
|
|
||||||
|
|
||||||
if (onScreenSlotRendered && screenSlot) {
|
|
||||||
// assuming there is only ever one screen slot
|
|
||||||
onScreenSlotRendered(screenSlot)
|
|
||||||
}
|
|
||||||
|
|
||||||
treeNode.children = childNodes
|
|
||||||
|
|
||||||
return childNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitName = fullname => {
|
|
||||||
const nameParts = fullname.split("/")
|
|
||||||
|
|
||||||
const componentName = nameParts[nameParts.length - 1]
|
|
||||||
|
|
||||||
const libName = fullname.substring(
|
|
||||||
0,
|
|
||||||
fullname.length - componentName.length - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
return { libName, componentName }
|
|
||||||
}
|
|
||||||
|
|
||||||
const areTreeNodesEqual = (children1, children2) => {
|
|
||||||
if (children1.length !== children2.length) return false
|
|
||||||
if (children1 === children2) return true
|
|
||||||
|
|
||||||
let isEqual = false
|
|
||||||
for (let i = 0; i < children1.length; i++) {
|
|
||||||
// same context and same children, then nothing has changed
|
|
||||||
isEqual =
|
|
||||||
deepEqual(children1[i].context, children2[i].context) &&
|
|
||||||
areTreeNodesEqual(children1[i].children, children2[i].children)
|
|
||||||
if (!isEqual) return false
|
|
||||||
if (isScreenSlot(children1[i].parentNode.props._component)) {
|
|
||||||
isEqual = deepEqual(children1[i].props, children2[i].props)
|
|
||||||
}
|
|
||||||
if (!isEqual) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { screenSlotComponent } from "./screenSlotComponent"
|
|
||||||
|
|
||||||
export const builtinLibName = "##builtin"
|
|
||||||
|
|
||||||
export const isScreenSlot = componentName =>
|
|
||||||
componentName === "##builtin/screenslot"
|
|
||||||
|
|
||||||
export const builtins = window => ({
|
|
||||||
screenslot: screenSlotComponent(window),
|
|
||||||
})
|
|
|
@ -1,88 +0,0 @@
|
||||||
import renderTemplateString from "../state/renderTemplateString"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import hasBinding from "../state/hasBinding"
|
|
||||||
|
|
||||||
export const prepareRenderComponent = ({
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
props,
|
|
||||||
parentNode,
|
|
||||||
contextStoreKey,
|
|
||||||
}) => {
|
|
||||||
const thisNode = createTreeNode()
|
|
||||||
thisNode.parentNode = parentNode
|
|
||||||
thisNode.props = props
|
|
||||||
thisNode.contextStoreKey = contextStoreKey
|
|
||||||
|
|
||||||
// the treeNode is first created (above), and then this
|
|
||||||
// render method is add. The treeNode is returned, and
|
|
||||||
// render is called later (in attachChildren)
|
|
||||||
thisNode.render = initialProps => {
|
|
||||||
thisNode.component = new ComponentConstructor({
|
|
||||||
target: htmlElement,
|
|
||||||
props: initialProps,
|
|
||||||
hydrate: false,
|
|
||||||
anchor,
|
|
||||||
})
|
|
||||||
|
|
||||||
// finds the root element of the component, which was created by the contructor above
|
|
||||||
// we use this later to attach a className to. This is how styles
|
|
||||||
// are applied by the builder
|
|
||||||
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
|
|
||||||
|
|
||||||
let [componentName] = props._component.match(/[a-z]*$/)
|
|
||||||
if (props._id && thisNode.rootElement) {
|
|
||||||
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make this node listen to the store
|
|
||||||
if (thisNode.stateBound) {
|
|
||||||
const unsubscribe = appStore.subscribe(state => {
|
|
||||||
const storeBoundProps = Object.keys(initialProps._bb.props).filter(p =>
|
|
||||||
hasBinding(initialProps._bb.props[p])
|
|
||||||
)
|
|
||||||
if (storeBoundProps.length > 0) {
|
|
||||||
const toSet = {}
|
|
||||||
for (let prop of storeBoundProps) {
|
|
||||||
const propValue = initialProps._bb.props[prop]
|
|
||||||
toSet[prop] = renderTemplateString(propValue, state)
|
|
||||||
}
|
|
||||||
thisNode.component.$set(toSet)
|
|
||||||
}
|
|
||||||
}, thisNode.contextStoreKey)
|
|
||||||
thisNode.unsubscribe = unsubscribe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return thisNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTreeNode = () => ({
|
|
||||||
context: {},
|
|
||||||
props: {},
|
|
||||||
rootElement: null,
|
|
||||||
parentNode: null,
|
|
||||||
children: [],
|
|
||||||
bindings: [],
|
|
||||||
component: null,
|
|
||||||
unsubscribe: () => {},
|
|
||||||
render: () => {},
|
|
||||||
get destroy() {
|
|
||||||
const node = this
|
|
||||||
return () => {
|
|
||||||
if (node.children) {
|
|
||||||
// destroy children first - from leaf nodes up
|
|
||||||
for (let child of node.children) {
|
|
||||||
child.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.unsubscribe) node.unsubscribe()
|
|
||||||
if (node.component && node.component.$destroy) node.component.$destroy()
|
|
||||||
for (let onDestroyItem of node.onDestroy) {
|
|
||||||
onDestroyItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDestroy: [],
|
|
||||||
})
|
|
|
@ -1,122 +0,0 @@
|
||||||
import regexparam from "regexparam"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import { getAppId } from "@budibase/component-sdk"
|
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
|
||||||
function sanitize(url) {
|
|
||||||
if (!url) return url
|
|
||||||
return url
|
|
||||||
.split("/")
|
|
||||||
.map(part => {
|
|
||||||
// if parameter, then use as is
|
|
||||||
if (part.startsWith(":")) return part
|
|
||||||
return encodeURIComponent(part)
|
|
||||||
})
|
|
||||||
.join("/")
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRunningLocally = () => {
|
|
||||||
const hostname = (window.location && window.location.hostname) || ""
|
|
||||||
return (
|
|
||||||
hostname === "localhost" ||
|
|
||||||
hostname === "127.0.0.1" ||
|
|
||||||
hostname.startsWith("192.168")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeRootedPath = url => {
|
|
||||||
if (isRunningLocally()) {
|
|
||||||
const appId = getAppId()
|
|
||||||
if (url) {
|
|
||||||
url = sanitize(url)
|
|
||||||
if (!url.startsWith("/")) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
if (url.startsWith(`/${appId}`)) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
return `/${appId}${url}`
|
|
||||||
}
|
|
||||||
return `/${appId}`
|
|
||||||
}
|
|
||||||
return sanitize(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = screens.map(s => makeRootedPath(s.routing?.route))
|
|
||||||
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
|
|
||||||
if (fallback < 0) fallback = 0
|
|
||||||
|
|
||||||
let current
|
|
||||||
|
|
||||||
function route(url) {
|
|
||||||
const _url = makeRootedPath(url.state || url)
|
|
||||||
current = routes.findIndex(
|
|
||||||
p =>
|
|
||||||
p !== makeRootedPath("*") &&
|
|
||||||
new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
const params = {}
|
|
||||||
|
|
||||||
if (current === -1) {
|
|
||||||
routes.forEach((p, i) => {
|
|
||||||
// ignore home - which matched everything
|
|
||||||
if (p === makeRootedPath("*")) return
|
|
||||||
const pm = regexparam(p)
|
|
||||||
const matches = pm.pattern.exec(_url)
|
|
||||||
|
|
||||||
if (!matches) return
|
|
||||||
|
|
||||||
let j = 0
|
|
||||||
while (j < pm.keys.length) {
|
|
||||||
params[pm.keys[j]] = matches[++j] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
current = i
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state["##routeParams"] = params
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
const screenIndex = current !== -1 ? current : fallback
|
|
||||||
|
|
||||||
try {
|
|
||||||
!url.state && history.pushState(_url, null, _url)
|
|
||||||
} catch (_) {
|
|
||||||
// ignoring an exception here as the builder runs an iframe, which does not like this
|
|
||||||
}
|
|
||||||
|
|
||||||
onScreenSelected(screens[screenIndex], _url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function click(e) {
|
|
||||||
const x = e.target.closest("a")
|
|
||||||
const y = x && x.getAttribute("href")
|
|
||||||
|
|
||||||
if (
|
|
||||||
e.ctrlKey ||
|
|
||||||
e.metaKey ||
|
|
||||||
e.altKey ||
|
|
||||||
e.shiftKey ||
|
|
||||||
e.button ||
|
|
||||||
e.defaultPrevented
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
const target = (x && x.target) || "_self"
|
|
||||||
if (!y || target !== "_self" || x.host !== location.host) return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
route(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener("popstate", route)
|
|
||||||
addEventListener("pushstate", route)
|
|
||||||
addEventListener("click", click)
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
export const screenSlotComponent = window => {
|
|
||||||
return function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
const $set = props => {
|
|
||||||
props._bb.attachChildren(node)
|
|
||||||
}
|
|
||||||
const $destroy = () => {
|
|
||||||
if (opts.target && node) opts.target.removeChild(node)
|
|
||||||
}
|
|
||||||
this.$set = $set
|
|
||||||
this.$destroy = $destroy
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import setBindableComponentProp from "./setBindableComponentProp"
|
|
||||||
import { attachChildren } from "../render/attachChildren"
|
|
||||||
import store from "./store"
|
|
||||||
|
|
||||||
export const bbFactory = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
}) => {
|
|
||||||
return (treeNode, setupState) => {
|
|
||||||
const attachParams = {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
attachChildren: attachChildren(attachParams),
|
|
||||||
props: treeNode.props,
|
|
||||||
call: async eventName =>
|
|
||||||
eventName &&
|
|
||||||
(await runEventActions(
|
|
||||||
treeNode.props[eventName],
|
|
||||||
store.getState(treeNode.contextStoreKey)
|
|
||||||
)),
|
|
||||||
setBinding: setBindableComponentProp(treeNode),
|
|
||||||
parent,
|
|
||||||
store: store.getStore(treeNode.contextStoreKey),
|
|
||||||
// these parameters are populated by screenRouter
|
|
||||||
routeParams: () => store.getState()["##routeParams"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
import { updateRow, saveRow, deleteRow } from "@budibase/component-sdk"
|
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
|
||||||
|
|
||||||
export const eventHandlers = routeTo => {
|
|
||||||
const handlers = {
|
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
|
||||||
"Update Row": updateRow,
|
|
||||||
"Save Row": saveRow,
|
|
||||||
"Delete Row": deleteRow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// when an event is called, this is what gets run
|
|
||||||
const runEventActions = async (actions, state) => {
|
|
||||||
if (!actions) return
|
|
||||||
// calls event handlers sequentially
|
|
||||||
for (let action of actions) {
|
|
||||||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
|
||||||
const parameters = createParameters(action.parameters, state)
|
|
||||||
if (handler) {
|
|
||||||
await handler(parameters, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runEventActions
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will take a parameters obj, iterate all keys, and do a mustache render
|
|
||||||
// for every string. It will work recursively if it encounnters an {}
|
|
||||||
const createParameters = (parameterTemplateObj, state) => {
|
|
||||||
const parameters = {}
|
|
||||||
for (let key in parameterTemplateObj) {
|
|
||||||
if (typeof parameterTemplateObj[key] === "string") {
|
|
||||||
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
|
|
||||||
} else if (typeof parameterTemplateObj[key] === "object") {
|
|
||||||
parameters[key] = createParameters(parameterTemplateObj[key], state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parameters
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const setContext = treeNode => (key, value) =>
|
|
||||||
(treeNode.context[key] = value)
|
|
||||||
|
|
||||||
export const getContext = treeNode => key => {
|
|
||||||
if (treeNode.context && treeNode.context[key] !== undefined)
|
|
||||||
return treeNode.context[key]
|
|
||||||
|
|
||||||
if (!treeNode.context.$parent) return
|
|
||||||
|
|
||||||
return getContext(treeNode.parentNode)(key)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export default value => typeof value === "string" && value.includes("{{")
|
|
|
@ -1,17 +0,0 @@
|
||||||
import mustache from "mustache"
|
|
||||||
|
|
||||||
// this is a much more liberal version of mustache's escape function
|
|
||||||
// ...just ignoring < and > to prevent tags from user input
|
|
||||||
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
|
||||||
|
|
||||||
const entityMap = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
}
|
|
||||||
|
|
||||||
mustache.escape = text =>
|
|
||||||
String(text).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
|
|
||||||
return entityMap[s] || s
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mustache.render
|
|
|
@ -1,13 +0,0 @@
|
||||||
import appStore from "./store"
|
|
||||||
|
|
||||||
export default treeNode => (propName, value) => {
|
|
||||||
if (!propName || propName.length === 0) return
|
|
||||||
if (!treeNode) return
|
|
||||||
const componentId = treeNode.props._id
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state[componentId] = state[componentId] || {}
|
|
||||||
state[componentId][propName] = value
|
|
||||||
return state
|
|
||||||
}, treeNode.contextStoreKey)
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { eventHandlers } from "./eventHandlers"
|
|
||||||
import { bbFactory } from "./bbComponentApi"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
import appStore from "./store"
|
|
||||||
import hasBinding from "./hasBinding"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
|
||||||
doNothing.isPlaceholder = true
|
|
||||||
|
|
||||||
const isMetaProp = propName =>
|
|
||||||
propName === "_component" ||
|
|
||||||
propName === "_children" ||
|
|
||||||
propName === "_id" ||
|
|
||||||
propName === "_style" ||
|
|
||||||
propName === "_code" ||
|
|
||||||
propName === "_codeMeta" ||
|
|
||||||
propName === "_styles"
|
|
||||||
|
|
||||||
export const createStateManager = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
routeTo,
|
|
||||||
}) => {
|
|
||||||
let runEventActions = eventHandlers(routeTo)
|
|
||||||
|
|
||||||
const bb = bbFactory({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
})
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
|
|
||||||
return {
|
|
||||||
setup,
|
|
||||||
destroy: () => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _setup = bb => node => {
|
|
||||||
const props = node.props
|
|
||||||
const initialProps = { ...props }
|
|
||||||
|
|
||||||
for (let propName in props) {
|
|
||||||
if (isMetaProp(propName)) continue
|
|
||||||
|
|
||||||
const propValue = props[propName]
|
|
||||||
|
|
||||||
const isBound = hasBinding(propValue)
|
|
||||||
|
|
||||||
if (isBound) {
|
|
||||||
const state = appStore.getState(node.contextStoreKey)
|
|
||||||
initialProps[propName] = renderTemplateString(propValue, state)
|
|
||||||
|
|
||||||
if (!node.stateBound) {
|
|
||||||
node.stateBound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
initialProps._bb = bb(node, setup)
|
|
||||||
|
|
||||||
return initialProps
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
// we assume that the reference to this state object
|
|
||||||
// will remain for the life of the application
|
|
||||||
const rootState = {}
|
|
||||||
const rootStore = writable(rootState)
|
|
||||||
const contextStores = {}
|
|
||||||
|
|
||||||
// contextProviderId is the component id that provides the data for the context
|
|
||||||
const contextStoreKey = (dataProviderId, childIndex) =>
|
|
||||||
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}`
|
|
||||||
|
|
||||||
// creates a store for a datacontext (e.g. each item in a list component)
|
|
||||||
// overrides store if already exists
|
|
||||||
const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
|
|
||||||
const key = contextStoreKey(dataProviderId, childIndex)
|
|
||||||
const state = { data }
|
|
||||||
|
|
||||||
// add reference to parent state object,
|
|
||||||
// so we can use bindings like state.parent.parent
|
|
||||||
// (if no parent, then parent is rootState )
|
|
||||||
state.parent = parentContextStoreId
|
|
||||||
? contextStores[parentContextStoreId].state
|
|
||||||
: rootState
|
|
||||||
|
|
||||||
contextStores[key] = {
|
|
||||||
store: writable(state),
|
|
||||||
subscriberCount: 0,
|
|
||||||
state,
|
|
||||||
parentContextStoreId,
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscribe = (subscription, storeKey) => {
|
|
||||||
if (!storeKey) {
|
|
||||||
return rootStore.subscribe(subscription)
|
|
||||||
}
|
|
||||||
const contextStore = contextStores[storeKey]
|
|
||||||
|
|
||||||
// we are subscribing to multiple stores,
|
|
||||||
// we dont want to run our listener for every subscription, the first time
|
|
||||||
// as this could repeatedly run $set on the same component
|
|
||||||
// ... which already has its initial properties set properly
|
|
||||||
const ignoreFirstSubscription = () => {
|
|
||||||
let hasRunOnce = false
|
|
||||||
return () => {
|
|
||||||
if (hasRunOnce) subscription(contextStore.state)
|
|
||||||
hasRunOnce = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribes = [rootStore.subscribe(ignoreFirstSubscription())]
|
|
||||||
|
|
||||||
// we subscribe to all stores in the hierarchy
|
|
||||||
const ancestorSubscribe = ctxStore => {
|
|
||||||
// unsubscribe func returned by svelte store
|
|
||||||
const svelteUnsub = ctxStore.store.subscribe(ignoreFirstSubscription())
|
|
||||||
|
|
||||||
// we wrap the svelte unsubscribe, so we can
|
|
||||||
// cleanup stores when they are no longer subscribed to
|
|
||||||
const unsub = () => {
|
|
||||||
ctxStore.subscriberCount = contextStore.subscriberCount - 1
|
|
||||||
// when no subscribers left, we delete the store
|
|
||||||
if (ctxStore.subscriberCount === 0) {
|
|
||||||
delete ctxStore[storeKey]
|
|
||||||
}
|
|
||||||
svelteUnsub()
|
|
||||||
}
|
|
||||||
unsubscribes.push(unsub)
|
|
||||||
if (ctxStore.parentContextStoreId) {
|
|
||||||
ancestorSubscribe(contextStores[ctxStore.parentContextStoreId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ancestorSubscribe(contextStore)
|
|
||||||
|
|
||||||
// our final unsubscribe function calls unsubscribe on all stores
|
|
||||||
return () => unsubscribes.forEach(u => u())
|
|
||||||
}
|
|
||||||
|
|
||||||
const findStore = (dataProviderId, childIndex) =>
|
|
||||||
dataProviderId
|
|
||||||
? contextStores[contextStoreKey(dataProviderId, childIndex)].store
|
|
||||||
: rootStore
|
|
||||||
|
|
||||||
const update = (updatefunc, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).update(updatefunc)
|
|
||||||
|
|
||||||
const set = (value, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).set(value)
|
|
||||||
|
|
||||||
const getState = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
|
||||||
|
|
||||||
const getStore = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].store : rootStore
|
|
||||||
|
|
||||||
export default {
|
|
||||||
subscribe,
|
|
||||||
update,
|
|
||||||
set,
|
|
||||||
getState,
|
|
||||||
create,
|
|
||||||
contextStoreKey,
|
|
||||||
getStore,
|
|
||||||
}
|
|
|
@ -39,7 +39,7 @@ const MAIN = {
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
||||||
_component: "@budibase/standard-components/Navigation",
|
_component: "@budibase/standard-components/navigation",
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {
|
normal: {
|
||||||
"max-width": "1400px",
|
"max-width": "1400px",
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"embed": "string"
|
"embed": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"navigation": {
|
||||||
"name": "Navigation",
|
"name": "Navigation",
|
||||||
"description": "A basic header navigation component",
|
"description": "A basic header navigation component",
|
||||||
"children": true,
|
"children": true,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export { default as button } from "./Button.svelte"
|
||||||
export { default as login } from "./Login.svelte"
|
export { default as login } from "./Login.svelte"
|
||||||
export { default as link } from "./Link.svelte"
|
export { default as link } from "./Link.svelte"
|
||||||
export { default as image } from "./Image.svelte"
|
export { default as image } from "./Image.svelte"
|
||||||
export { default as Navigation } from "./Navigation.svelte"
|
export { default as navigation } from "./Navigation.svelte"
|
||||||
export { default as datagrid } from "./grid/Component.svelte"
|
export { default as datagrid } from "./grid/Component.svelte"
|
||||||
export { default as dataform } from "./DataForm.svelte"
|
export { default as dataform } from "./DataForm.svelte"
|
||||||
export { default as dataformwide } from "./DataFormWide.svelte"
|
export { default as dataformwide } from "./DataFormWide.svelte"
|
||||||
|
|
Loading…
Reference in New Issue