forked from tsai/budibase
23 changed files with 30 additions and 821 deletions
@ -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, |
|
||||
} |
|
||||
Loading…
Reference in new issue