Add fade screen transition and fix navigation component casing
This commit is contained in:
parent
dc010cd769
commit
07229858c8
|
@ -479,7 +479,7 @@ export const getFrontendStore = () => {
|
|||
// Try to extract a nav component from the master screen
|
||||
const nav = findChildComponentType(
|
||||
state.pages.main,
|
||||
"@budibase/standard-components/Navigation"
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (nav) {
|
||||
let newLink
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/old/state/eventHandlers"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const eventTypeKey = "##eventHandlerType"
|
||||
|
||||
export let event
|
||||
|
||||
|
@ -18,8 +18,7 @@
|
|||
$: actions = event || []
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
||||
.component
|
||||
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
||||
|
||||
const updateEventHandler = (updatedHandler, index) => {
|
||||
actions[index] = updatedHandler
|
||||
|
@ -33,7 +32,7 @@
|
|||
const addAction = actionType => () => {
|
||||
const newAction = {
|
||||
parameters: {},
|
||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
||||
[eventTypeKey]: actionType.name,
|
||||
}
|
||||
actions.push(newAction)
|
||||
selectedAction = newAction
|
||||
|
@ -79,7 +78,7 @@
|
|||
{#each actions as action, index}
|
||||
<div class="action-container">
|
||||
<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}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
|
|
|
@ -1175,7 +1175,7 @@ export default {
|
|||
},
|
||||
{
|
||||
name: "Nav Bar",
|
||||
_component: "@budibase/standard-components/Navigation",
|
||||
_component: "@budibase/standard-components/navigation",
|
||||
description:
|
||||
"A component for handling the navigation within your app.",
|
||||
icon: "ri-navigation-line",
|
||||
|
|
|
@ -30,3 +30,9 @@
|
|||
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { screenStore, routeStore } from "../store"
|
||||
import Component from "./Component.svelte"
|
||||
|
||||
|
@ -17,5 +18,18 @@
|
|||
</script>
|
||||
|
||||
{#each screens as screen (screen._id)}
|
||||
<div in:fade>
|
||||
<Component definition={screen} />
|
||||
</div>
|
||||
{/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: [
|
||||
{
|
||||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
||||
_component: "@budibase/standard-components/Navigation",
|
||||
_component: "@budibase/standard-components/navigation",
|
||||
_styles: {
|
||||
normal: {
|
||||
"max-width": "1400px",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"embed": "string"
|
||||
}
|
||||
},
|
||||
"Navigation": {
|
||||
"navigation": {
|
||||
"name": "Navigation",
|
||||
"description": "A basic header navigation component",
|
||||
"children": true,
|
||||
|
|
|
@ -10,7 +10,7 @@ export { default as button } from "./Button.svelte"
|
|||
export { default as login } from "./Login.svelte"
|
||||
export { default as link } from "./Link.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 dataform } from "./DataForm.svelte"
|
||||
export { default as dataformwide } from "./DataFormWide.svelte"
|
||||
|
|
Loading…
Reference in New Issue