Merge pull request #8376 from Budibase/cheeks-lab-day-grid
Grid component + builder performance improvements
This commit is contained in:
commit
75aae5b7e0
|
@ -182,7 +182,70 @@ export const getFrontendStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
validate: screen => {
|
||||||
|
// Recursive function to find any illegal children in component trees
|
||||||
|
const findIllegalChild = (
|
||||||
|
component,
|
||||||
|
illegalChildren = [],
|
||||||
|
legalDirectChildren = []
|
||||||
|
) => {
|
||||||
|
const type = component._component
|
||||||
|
if (illegalChildren.includes(type)) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
legalDirectChildren.length &&
|
||||||
|
!legalDirectChildren.includes(type)
|
||||||
|
) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
if (!component?._children?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
component._component
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset whitelist for direct children
|
||||||
|
legalDirectChildren = []
|
||||||
|
if (definition?.legalDirectChildren?.length) {
|
||||||
|
legalDirectChildren = definition.legalDirectChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append blacklisted components and remove duplicates
|
||||||
|
if (definition?.illegalChildren?.length) {
|
||||||
|
const blacklist = definition.illegalChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
illegalChildren = [...new Set([...illegalChildren, ...blacklist])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse on all children
|
||||||
|
for (let child of component._children) {
|
||||||
|
const illegalChild = findIllegalChild(
|
||||||
|
child,
|
||||||
|
illegalChildren,
|
||||||
|
legalDirectChildren
|
||||||
|
)
|
||||||
|
if (illegalChild) {
|
||||||
|
return illegalChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the entire tree and throw an error if an illegal child is
|
||||||
|
// found anywhere
|
||||||
|
const illegalChild = findIllegalChild(screen.props)
|
||||||
|
if (illegalChild) {
|
||||||
|
const def = store.actions.components.getDefinition(illegalChild)
|
||||||
|
throw `You can't place a ${def.name} here`
|
||||||
|
}
|
||||||
|
},
|
||||||
save: async screen => {
|
save: async screen => {
|
||||||
|
store.actions.screens.validate(screen)
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const creatingNewScreen = screen._id === undefined
|
const creatingNewScreen = screen._id === undefined
|
||||||
const savedScreen = await API.saveScreen(screen)
|
const savedScreen = await API.saveScreen(screen)
|
||||||
|
@ -445,7 +508,11 @@ export const getFrontendStore = () => {
|
||||||
return {
|
return {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
_component: definition.component,
|
_component: definition.component,
|
||||||
_styles: { normal: {}, hover: {}, active: {} },
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
||||||
...cloneDeep(props),
|
...cloneDeep(props),
|
||||||
...extras,
|
...extras,
|
||||||
|
@ -533,12 +600,11 @@ export const getFrontendStore = () => {
|
||||||
},
|
},
|
||||||
patch: async (patchFn, componentId, screenId) => {
|
patch: async (patchFn, componentId, screenId) => {
|
||||||
// Use selected component by default
|
// Use selected component by default
|
||||||
if (!componentId && !screenId) {
|
if (!componentId || !screenId) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
componentId = state.selectedComponentId
|
componentId = componentId || state.selectedComponentId
|
||||||
screenId = state.selectedScreenId
|
screenId = screenId || state.selectedScreenId
|
||||||
}
|
}
|
||||||
// Invalid if only a screen or component ID provided
|
|
||||||
if (!componentId || !screenId || !patchFn) {
|
if (!componentId || !screenId || !patchFn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -601,16 +667,14 @@ export const getFrontendStore = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select the parent if cutting
|
// Select the parent if cutting
|
||||||
if (cut) {
|
if (cut && selectParent) {
|
||||||
const screen = get(selectedScreen)
|
const screen = get(selectedScreen)
|
||||||
const parent = findComponentParent(screen?.props, component._id)
|
const parent = findComponentParent(screen?.props, component._id)
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (selectParent) {
|
store.update(state => {
|
||||||
store.update(state => {
|
state.selectedComponentId = parent._id
|
||||||
state.selectedComponentId = parent._id
|
return state
|
||||||
return state
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -621,16 +685,24 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
let newComponentId
|
let newComponentId
|
||||||
|
|
||||||
|
// Remove copied component if cutting, regardless if pasting works
|
||||||
|
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||||
|
if (componentToPaste.isCut) {
|
||||||
|
store.update(state => {
|
||||||
|
delete state.componentToPaste
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Patch screen
|
// Patch screen
|
||||||
const patch = screen => {
|
const patch = screen => {
|
||||||
// Get up to date ref to target
|
// Get up to date ref to target
|
||||||
targetComponent = findComponent(screen.props, targetComponent._id)
|
targetComponent = findComponent(screen.props, targetComponent._id)
|
||||||
if (!targetComponent) {
|
if (!targetComponent) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
const cut = state.componentToPaste.isCut
|
const cut = componentToPaste.isCut
|
||||||
const originalId = state.componentToPaste._id
|
const originalId = componentToPaste._id
|
||||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
|
||||||
delete componentToPaste.isCut
|
delete componentToPaste.isCut
|
||||||
|
|
||||||
// Make new component unique if copying
|
// Make new component unique if copying
|
||||||
|
@ -685,11 +757,8 @@ export const getFrontendStore = () => {
|
||||||
const targetScreenId = targetScreen?._id || state.selectedScreenId
|
const targetScreenId = targetScreen?._id || state.selectedScreenId
|
||||||
await store.actions.screens.patch(patch, targetScreenId)
|
await store.actions.screens.patch(patch, targetScreenId)
|
||||||
|
|
||||||
|
// Select the new component
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Remove copied component if cutting
|
|
||||||
if (state.componentToPaste.isCut) {
|
|
||||||
delete state.componentToPaste
|
|
||||||
}
|
|
||||||
state.selectedScreenId = targetScreenId
|
state.selectedScreenId = targetScreenId
|
||||||
state.selectedComponentId = newComponentId
|
state.selectedComponentId = newComponentId
|
||||||
return state
|
return state
|
||||||
|
@ -893,6 +962,15 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateStyles: async (styles, id) => {
|
||||||
|
const patchFn = component => {
|
||||||
|
component._styles.normal = {
|
||||||
|
...component._styles.normal,
|
||||||
|
...styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await store.actions.components.patch(patchFn, id)
|
||||||
|
},
|
||||||
updateCustomStyle: async style => {
|
updateCustomStyle: async style => {
|
||||||
await store.actions.components.patch(component => {
|
await store.actions.components.patch(component => {
|
||||||
component._styles.custom = style
|
component._styles.custom = style
|
||||||
|
|
|
@ -86,7 +86,11 @@
|
||||||
: [],
|
: [],
|
||||||
isBudibaseEvent: true,
|
isBudibaseEvent: true,
|
||||||
usedPlugins: $store.usedPlugins,
|
usedPlugins: $store.usedPlugins,
|
||||||
location: window.location,
|
location: {
|
||||||
|
protocol: window.location.protocol,
|
||||||
|
hostname: window.location.hostname,
|
||||||
|
port: window.location.port,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the preview when required
|
// Refresh the preview when required
|
||||||
|
@ -99,7 +103,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register handler to send custom to the preview
|
// Register handler to send custom to the preview
|
||||||
$: store.actions.preview.registerEventHandler((name, payload) => {
|
$: sendPreviewEvent = (name, payload) => {
|
||||||
iframe?.contentWindow.postMessage(
|
iframe?.contentWindow.postMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name,
|
name,
|
||||||
|
@ -108,120 +112,116 @@
|
||||||
runtimeEvent: true,
|
runtimeEvent: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
$: store.actions.preview.registerEventHandler(sendPreviewEvent)
|
||||||
|
|
||||||
// Update the iframe with the builder info to render the correct preview
|
// Update the iframe with the builder info to render the correct preview
|
||||||
const refreshContent = message => {
|
const refreshContent = message => {
|
||||||
iframe?.contentWindow.postMessage(message)
|
iframe?.contentWindow.postMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiveMessage = message => {
|
const receiveMessage = async message => {
|
||||||
const handlers = {
|
if (!message?.data?.type) {
|
||||||
[MessageTypes.READY]: () => {
|
|
||||||
// Initialise the app when mounted
|
|
||||||
if ($store.clientFeatures.messagePassing) {
|
|
||||||
if (!loading) return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display preview immediately if the intelligent loading feature
|
|
||||||
// is not supported
|
|
||||||
if (!$store.clientFeatures.intelligentLoading) {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
refreshContent(json)
|
|
||||||
},
|
|
||||||
[MessageTypes.ERROR]: event => {
|
|
||||||
// Catch any app errors
|
|
||||||
loading = false
|
|
||||||
error = event.error || "An unknown error occurred"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
|
|
||||||
messageHandler(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBudibaseEvent = async event => {
|
|
||||||
const { type, data } = event.data || event.detail
|
|
||||||
if (!type) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await the event handler
|
||||||
try {
|
try {
|
||||||
if (type === "select-component" && data.id) {
|
await handleBudibaseEvent(message)
|
||||||
$store.selectedComponentId = data.id
|
|
||||||
if (!$isActive("./components")) {
|
|
||||||
$goto("./components")
|
|
||||||
}
|
|
||||||
} else if (type === "update-prop") {
|
|
||||||
await store.actions.components.updateSetting(data.prop, data.value)
|
|
||||||
} else if (type === "delete-component" && data.id) {
|
|
||||||
// Legacy type, can be deleted in future
|
|
||||||
confirmDeleteComponent(data.id)
|
|
||||||
} else if (type === "key-down") {
|
|
||||||
const { key, ctrlKey } = data
|
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
|
||||||
} else if (type === "duplicate-component" && data.id) {
|
|
||||||
const rootComponent = get(currentAsset).props
|
|
||||||
const component = findComponent(rootComponent, data.id)
|
|
||||||
store.actions.components.copy(component)
|
|
||||||
await store.actions.components.paste(component)
|
|
||||||
} else if (type === "preview-loaded") {
|
|
||||||
// Wait for this event to show the client library if intelligent
|
|
||||||
// loading is supported
|
|
||||||
loading = false
|
|
||||||
} else if (type === "move-component") {
|
|
||||||
const { componentId, destinationComponentId } = data
|
|
||||||
const rootComponent = get(currentAsset).props
|
|
||||||
|
|
||||||
// Get source and destination components
|
|
||||||
const source = findComponent(rootComponent, componentId)
|
|
||||||
const destination = findComponent(rootComponent, destinationComponentId)
|
|
||||||
|
|
||||||
// Stop if the target is a child of source
|
|
||||||
const path = findComponentPath(source, destinationComponentId)
|
|
||||||
const ids = path.map(component => component._id)
|
|
||||||
if (ids.includes(data.destinationComponentId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut and paste the component to the new destination
|
|
||||||
if (source && destination) {
|
|
||||||
store.actions.components.copy(source, true)
|
|
||||||
await store.actions.components.paste(destination, data.mode)
|
|
||||||
}
|
|
||||||
} else if (type === "click-nav") {
|
|
||||||
if (!$isActive("./navigation")) {
|
|
||||||
$goto("./navigation")
|
|
||||||
}
|
|
||||||
} else if (type === "request-add-component") {
|
|
||||||
toggleAddComponent()
|
|
||||||
} else if (type === "highlight-setting") {
|
|
||||||
store.actions.settings.highlight(data.setting)
|
|
||||||
|
|
||||||
// Also scroll setting into view
|
|
||||||
const selector = `[data-cy="${data.setting}-prop-control"`
|
|
||||||
const element = document.querySelector(selector)?.parentElement
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (type === "eject-block") {
|
|
||||||
const { id, definition } = data
|
|
||||||
await store.actions.components.handleEjectBlock(id, definition)
|
|
||||||
} else if (type === "reload-plugin") {
|
|
||||||
await store.actions.components.refreshDefinitions()
|
|
||||||
} else if (type === "drop-new-component") {
|
|
||||||
const { component, parent, index } = data
|
|
||||||
await store.actions.components.create(component, null, parent, index)
|
|
||||||
} else {
|
|
||||||
console.warn(`Client sent unknown event type: ${type}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
notifications.error(error || "Error handling event from app preview")
|
||||||
notifications.error("Error handling event from app preview")
|
}
|
||||||
|
|
||||||
|
// Reply that the event has been completed
|
||||||
|
if (message.data?.id) {
|
||||||
|
sendPreviewEvent("event-completed", message.data?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBudibaseEvent = async event => {
|
||||||
|
const { type, data } = event.data
|
||||||
|
if (type === MessageTypes.READY) {
|
||||||
|
// Initialise the app when mounted
|
||||||
|
if (!loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refreshContent(json)
|
||||||
|
} else if (type === MessageTypes.ERROR) {
|
||||||
|
// Catch any app errors
|
||||||
|
loading = false
|
||||||
|
error = event.error || "An unknown error occurred"
|
||||||
|
} else if (type === "select-component" && data.id) {
|
||||||
|
$store.selectedComponentId = data.id
|
||||||
|
if (!$isActive("./components")) {
|
||||||
|
$goto("./components")
|
||||||
|
}
|
||||||
|
} else if (type === "update-prop") {
|
||||||
|
await store.actions.components.updateSetting(data.prop, data.value)
|
||||||
|
} else if (type === "update-styles") {
|
||||||
|
await store.actions.components.updateStyles(data.styles, data.id)
|
||||||
|
} else if (type === "delete-component" && data.id) {
|
||||||
|
// Legacy type, can be deleted in future
|
||||||
|
confirmDeleteComponent(data.id)
|
||||||
|
} else if (type === "key-down") {
|
||||||
|
const { key, ctrlKey } = data
|
||||||
|
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
|
||||||
|
} else if (type === "duplicate-component" && data.id) {
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
const component = findComponent(rootComponent, data.id)
|
||||||
|
store.actions.components.copy(component)
|
||||||
|
await store.actions.components.paste(component)
|
||||||
|
} else if (type === "preview-loaded") {
|
||||||
|
// Wait for this event to show the client library if intelligent
|
||||||
|
// loading is supported
|
||||||
|
loading = false
|
||||||
|
} else if (type === "move-component") {
|
||||||
|
const { componentId, destinationComponentId } = data
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
|
||||||
|
// Get source and destination components
|
||||||
|
const source = findComponent(rootComponent, componentId)
|
||||||
|
const destination = findComponent(rootComponent, destinationComponentId)
|
||||||
|
|
||||||
|
// Stop if the target is a child of source
|
||||||
|
const path = findComponentPath(source, destinationComponentId)
|
||||||
|
const ids = path.map(component => component._id)
|
||||||
|
if (ids.includes(data.destinationComponentId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut and paste the component to the new destination
|
||||||
|
if (source && destination) {
|
||||||
|
store.actions.components.copy(source, true, false)
|
||||||
|
await store.actions.components.paste(destination, data.mode)
|
||||||
|
}
|
||||||
|
} else if (type === "click-nav") {
|
||||||
|
if (!$isActive("./navigation")) {
|
||||||
|
$goto("./navigation")
|
||||||
|
}
|
||||||
|
} else if (type === "request-add-component") {
|
||||||
|
toggleAddComponent()
|
||||||
|
} else if (type === "highlight-setting") {
|
||||||
|
store.actions.settings.highlight(data.setting)
|
||||||
|
|
||||||
|
// Also scroll setting into view
|
||||||
|
const selector = `[data-cy="${data.setting}-prop-control"`
|
||||||
|
const element = document.querySelector(selector)?.parentElement
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const { id, definition } = data
|
||||||
|
await store.actions.components.handleEjectBlock(id, definition)
|
||||||
|
} else if (type === "reload-plugin") {
|
||||||
|
await store.actions.components.refreshDefinitions()
|
||||||
|
} else if (type === "drop-new-component") {
|
||||||
|
const { component, parent, index } = data
|
||||||
|
await store.actions.components.create(component, null, parent, index)
|
||||||
|
} else {
|
||||||
|
console.warn(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,42 +254,10 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener("message", receiveMessage)
|
window.addEventListener("message", receiveMessage)
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
|
||||||
// Legacy - remove in later versions of BB
|
|
||||||
iframe.contentWindow.addEventListener(
|
|
||||||
"ready",
|
|
||||||
() => {
|
|
||||||
receiveMessage({ data: { type: MessageTypes.READY } })
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
)
|
|
||||||
iframe.contentWindow.addEventListener(
|
|
||||||
"error",
|
|
||||||
event => {
|
|
||||||
receiveMessage({
|
|
||||||
data: { type: MessageTypes.ERROR, error: event.detail },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
)
|
|
||||||
// Add listener for events sent by client library in preview
|
|
||||||
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove all iframe event listeners on component destroy
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
window.removeEventListener("message", receiveMessage)
|
window.removeEventListener("message", receiveMessage)
|
||||||
|
|
||||||
if (iframe.contentWindow) {
|
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
|
||||||
// Legacy - remove in later versions of BB
|
|
||||||
iframe.contentWindow.removeEventListener(
|
|
||||||
"bb-event",
|
|
||||||
handleBudibaseEvent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -80,10 +80,9 @@
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
return handler(component)
|
return await handler(component)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
notifications.error(error || "Error handling key press")
|
||||||
notifications.error("Error handling key press")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,34 +70,12 @@
|
||||||
closedNodes = closedNodes
|
closedNodes = closedNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrop = async (e, component) => {
|
const onDrop = async e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
const compDef = store.actions.components.getDefinition(
|
|
||||||
$dndStore.source?._component
|
|
||||||
)
|
|
||||||
if (!compDef) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const compTypeName = compDef.name.toLowerCase()
|
|
||||||
const path = findComponentPath(currentScreen.props, component._id)
|
|
||||||
|
|
||||||
for (let pathComp of path) {
|
|
||||||
const pathCompDef = store.actions.components.getDefinition(
|
|
||||||
pathComp?._component
|
|
||||||
)
|
|
||||||
if (pathCompDef?.illegalChildren?.indexOf(compTypeName) > -1) {
|
|
||||||
notifications.warning(
|
|
||||||
`${compDef.name} cannot be a child of ${pathCompDef.name} (${pathComp._instanceName})`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await dndStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
notifications.error(error || "Error saving component")
|
||||||
notifications.error("Error saving component")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +115,7 @@
|
||||||
on:dragstart={() => dndStore.actions.dragstart(component)}
|
on:dragstart={() => dndStore.actions.dragstart(component)}
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||||
on:drop={e => {
|
on:drop={onDrop}
|
||||||
onDrop(e, component)
|
|
||||||
}}
|
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={componentHasChildren(component)}
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import structure from "./componentStructure.json"
|
import structure from "./componentStructure.json"
|
||||||
import { store, selectedComponent } from "builderStore"
|
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
import { findComponentPath } from "builderStore/componentUtils"
|
||||||
|
|
||||||
let section = "components"
|
let section = "components"
|
||||||
let searchString
|
let searchString
|
||||||
|
@ -21,8 +22,10 @@
|
||||||
let selectedIndex
|
let selectedIndex
|
||||||
let componentList = []
|
let componentList = []
|
||||||
|
|
||||||
$: currentDefinition = store.actions.components.getDefinition(
|
$: allowedComponents = getAllowedComponents(
|
||||||
$selectedComponent?._component
|
$store.components,
|
||||||
|
$selectedScreen,
|
||||||
|
$selectedComponent
|
||||||
)
|
)
|
||||||
$: enrichedStructure = enrichStructure(
|
$: enrichedStructure = enrichStructure(
|
||||||
structure,
|
structure,
|
||||||
|
@ -31,13 +34,50 @@
|
||||||
)
|
)
|
||||||
$: filteredStructure = filterStructure(
|
$: filteredStructure = filterStructure(
|
||||||
enrichedStructure,
|
enrichedStructure,
|
||||||
section,
|
allowedComponents,
|
||||||
currentDefinition,
|
|
||||||
searchString
|
searchString
|
||||||
)
|
)
|
||||||
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
|
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children
|
||||||
$: orderMap = createComponentOrderMap(componentList)
|
$: orderMap = createComponentOrderMap(componentList)
|
||||||
|
|
||||||
|
const getAllowedComponents = (allComponents, screen, component) => {
|
||||||
|
const path = findComponentPath(screen?.props, component?._id)
|
||||||
|
if (!path?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial set of allowed components
|
||||||
|
let allowedComponents = []
|
||||||
|
const definition = store.actions.components.getDefinition(
|
||||||
|
component?._component
|
||||||
|
)
|
||||||
|
if (definition.legalDirectChildren?.length) {
|
||||||
|
allowedComponents = definition.legalDirectChildren.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allowedComponents = Object.keys(allComponents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build up list of illegal children from ancestors
|
||||||
|
let illegalChildren = definition.illegalChildren || []
|
||||||
|
path.forEach(ancestor => {
|
||||||
|
const def = store.actions.components.getDefinition(ancestor._component)
|
||||||
|
const blacklist = def?.illegalChildren?.map(x => {
|
||||||
|
return `@budibase/standard-components/${x}`
|
||||||
|
})
|
||||||
|
illegalChildren = [...illegalChildren, ...(blacklist || [])]
|
||||||
|
})
|
||||||
|
illegalChildren = [...new Set(illegalChildren)]
|
||||||
|
|
||||||
|
// Filter out illegal children from allowed components
|
||||||
|
allowedComponents = allowedComponents.filter(x => {
|
||||||
|
return !illegalChildren.includes(x)
|
||||||
|
})
|
||||||
|
|
||||||
|
return allowedComponents
|
||||||
|
}
|
||||||
|
|
||||||
// Creates a simple lookup map from an array, so we can find the selected
|
// Creates a simple lookup map from an array, so we can find the selected
|
||||||
// component much faster
|
// component much faster
|
||||||
const createComponentOrderMap = list => {
|
const createComponentOrderMap = list => {
|
||||||
|
@ -90,7 +130,7 @@
|
||||||
return enrichedStructure
|
return enrichedStructure
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterStructure = (structure, section, currentDefinition, search) => {
|
const filterStructure = (structure, allowedComponents, search) => {
|
||||||
selectedIndex = search ? 0 : null
|
selectedIndex = search ? 0 : null
|
||||||
componentList = []
|
componentList = []
|
||||||
if (!structure?.length) {
|
if (!structure?.length) {
|
||||||
|
@ -114,7 +154,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the component is allowed as a child
|
// Check if the component is allowed as a child
|
||||||
return !currentDefinition?.illegalChildren?.includes(name)
|
return allowedComponents.includes(child.component)
|
||||||
})
|
})
|
||||||
if (matchedChildren.length) {
|
if (matchedChildren.length) {
|
||||||
filteredStructure.push({
|
filteredStructure.push({
|
||||||
|
@ -138,7 +178,7 @@
|
||||||
await store.actions.components.create(component)
|
await store.actions.components.create(component)
|
||||||
$goto("../")
|
$goto("../")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error creating component")
|
notifications.error(error || "Error creating component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"icon": "ClassicGridView",
|
"icon": "ClassicGridView",
|
||||||
"children": [
|
"children": [
|
||||||
"container",
|
"container",
|
||||||
"section"
|
"section",
|
||||||
|
"grid"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
"showSettingsBar": true,
|
"showSettingsBar": true,
|
||||||
"size": {
|
"size": {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 200
|
||||||
},
|
},
|
||||||
"styles": [
|
"styles": [
|
||||||
"padding",
|
"padding",
|
||||||
|
@ -5037,6 +5037,45 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"name": "Grid (Beta)",
|
||||||
|
"icon": "ViewGrid",
|
||||||
|
"hasChildren": true,
|
||||||
|
"styles": [
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"illegalChildren": ["section", "grid"],
|
||||||
|
"legalDirectChildren": [
|
||||||
|
"container",
|
||||||
|
"tableblock",
|
||||||
|
"cardsblock",
|
||||||
|
"repeaterblock",
|
||||||
|
"formblock"
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"width": 800,
|
||||||
|
"height": 400
|
||||||
|
},
|
||||||
|
"showEmptyState": false,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Rows",
|
||||||
|
"key": "rows",
|
||||||
|
"defaultValue": 12,
|
||||||
|
"min": 1,
|
||||||
|
"max": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Columns",
|
||||||
|
"key": "cols",
|
||||||
|
"defaultValue": 12,
|
||||||
|
"min": 1,
|
||||||
|
"max": 32
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"formblock": {
|
"formblock": {
|
||||||
"name": "Form Block",
|
"name": "Form Block",
|
||||||
"icon": "Form",
|
"icon": "Form",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import DNDHandler from "components/preview/DNDHandler.svelte"
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
|
import GridDNDHandler from "components/preview/GridDNDHandler.svelte"
|
||||||
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
import KeyboardManager from "components/preview/KeyboardManager.svelte"
|
||||||
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
|
||||||
import DevTools from "components/devtools/DevTools.svelte"
|
import DevTools from "components/devtools/DevTools.svelte"
|
||||||
|
@ -196,6 +197,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<DNDHandler />
|
<DNDHandler />
|
||||||
|
<GridDNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</QueryParamsProvider>
|
</QueryParamsProvider>
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
devToolsStore,
|
devToolsStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
appStore,
|
appStore,
|
||||||
dndIsDragging,
|
|
||||||
dndComponentPath,
|
dndComponentPath,
|
||||||
|
dndIsDragging,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
import { getActiveConditions, reduceConditionActions } from "utils/conditions"
|
||||||
|
@ -90,6 +90,10 @@
|
||||||
let settingsDefinitionMap
|
let settingsDefinitionMap
|
||||||
let missingRequiredSettings = false
|
let missingRequiredSettings = false
|
||||||
|
|
||||||
|
// Temporary styles which can be added in the app preview for things like DND.
|
||||||
|
// We clear these whenever a new instance is received.
|
||||||
|
let ephemeralStyles
|
||||||
|
|
||||||
// Set up initial state for each new component instance
|
// Set up initial state for each new component instance
|
||||||
$: initialise(instance)
|
$: initialise(instance)
|
||||||
|
|
||||||
|
@ -171,6 +175,10 @@
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: {
|
styles: {
|
||||||
...instance._styles,
|
...instance._styles,
|
||||||
|
normal: {
|
||||||
|
...instance._styles?.normal,
|
||||||
|
...ephemeralStyles,
|
||||||
|
},
|
||||||
custom: customCSS,
|
custom: customCSS,
|
||||||
id,
|
id,
|
||||||
empty: emptyState,
|
empty: emptyState,
|
||||||
|
@ -449,6 +457,7 @@
|
||||||
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
getRawSettings: () => ({ ...staticSettings, ...dynamicSettings }),
|
||||||
getDataContext: () => get(context),
|
getDataContext: () => get(context),
|
||||||
reload: () => initialise(instance, true),
|
reload: () => initialise(instance, true),
|
||||||
|
setEphemeralStyles: styles => (ephemeralStyles = styles),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
|
|
||||||
|
export let cols = 12
|
||||||
|
export let rows = 12
|
||||||
|
|
||||||
|
// Deliberately non-reactive as we want this fixed whenever the grid renders
|
||||||
|
const defaultColSpan = Math.ceil((cols + 1) / 2)
|
||||||
|
const defaultRowSpan = Math.ceil((rows + 1) / 2)
|
||||||
|
|
||||||
|
$: coords = generateCoords(rows, cols)
|
||||||
|
|
||||||
|
const generateCoords = (rows, cols) => {
|
||||||
|
let grid = []
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
grid.push({ row, col })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid"
|
||||||
|
use:styleable={{
|
||||||
|
...$component.styles,
|
||||||
|
normal: {
|
||||||
|
...$component.styles?.normal,
|
||||||
|
"--cols": cols,
|
||||||
|
"--rows": rows,
|
||||||
|
"--default-col-span": defaultColSpan,
|
||||||
|
"--default-row-span": defaultRowSpan,
|
||||||
|
gap: "0 !important",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
data-rows={rows}
|
||||||
|
data-cols={cols}
|
||||||
|
>
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<div class="underlay">
|
||||||
|
{#each coords as coord}
|
||||||
|
<div class="placeholder" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
Ensure all children of containers which are top level children of
|
||||||
|
grids do not overflow
|
||||||
|
*/
|
||||||
|
:global(.grid > .component > .valid-container > .component > *) {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all top level children have some grid styles set */
|
||||||
|
:global(.grid > .component > *) {
|
||||||
|
overflow: hidden;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: var(--default-col-span);
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: var(--default-row-span);
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
.grid,
|
||||||
|
.underlay {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(var(--rows), 1fr);
|
||||||
|
grid-template-columns: repeat(var(--cols), 1fr);
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
grid-gap: 2px;
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
border: 2px solid var(--spectrum-global-color-gray-200);
|
||||||
|
}
|
||||||
|
.underlay {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
background-color: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -34,6 +34,7 @@ export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||||
export { default as tag } from "./Tag.svelte"
|
export { default as tag } from "./Tag.svelte"
|
||||||
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
|
export { default as grid } from "./Grid.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
|
@ -22,6 +22,18 @@
|
||||||
$: target = $dndStore.target
|
$: target = $dndStore.target
|
||||||
$: drop = $dndStore.drop
|
$: drop = $dndStore.drop
|
||||||
|
|
||||||
|
// Local flag for whether we are awaiting an async drop event
|
||||||
|
let dropping = false
|
||||||
|
|
||||||
|
// Util to check if a DND event originates from a grid (or inside a grid).
|
||||||
|
// This is important as we do not handle grid DND in this handler.
|
||||||
|
const isGridEvent = e => {
|
||||||
|
return e.target
|
||||||
|
?.closest?.(".component")
|
||||||
|
?.parentNode?.closest?.(".component")
|
||||||
|
?.childNodes[0]?.classList.contains("grid")
|
||||||
|
}
|
||||||
|
|
||||||
// Util to get the inner DOM node by a component ID
|
// Util to get the inner DOM node by a component ID
|
||||||
const getDOMNode = id => {
|
const getDOMNode = id => {
|
||||||
const component = document.getElementsByClassName(id)[0]
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
@ -41,6 +53,10 @@
|
||||||
|
|
||||||
// Callback when drag stops (whether dropped or not)
|
// Callback when drag stops (whether dropped or not)
|
||||||
const stopDragging = () => {
|
const stopDragging = () => {
|
||||||
|
if (dropping) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reset listener
|
// Reset listener
|
||||||
if (source?.id) {
|
if (source?.id) {
|
||||||
const component = document.getElementsByClassName(source?.id)[0]
|
const component = document.getElementsByClassName(source?.id)[0]
|
||||||
|
@ -55,6 +71,9 @@
|
||||||
|
|
||||||
// Callback when initially starting a drag on a draggable component
|
// Callback when initially starting a drag on a draggable component
|
||||||
const onDragStart = e => {
|
const onDragStart = e => {
|
||||||
|
if (isGridEvent(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const component = e.target.closest(".component")
|
const component = e.target.closest(".component")
|
||||||
if (!component?.classList.contains("draggable")) {
|
if (!component?.classList.contains("draggable")) {
|
||||||
return
|
return
|
||||||
|
@ -99,9 +118,9 @@
|
||||||
|
|
||||||
// Core logic for handling drop events and determining where to render the
|
// Core logic for handling drop events and determining where to render the
|
||||||
// drop target placeholder
|
// drop target placeholder
|
||||||
const processEvent = (mouseX, mouseY) => {
|
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
let { id, parent, node, acceptsChildren, empty } = target
|
let { id, parent, node, acceptsChildren, empty } = target
|
||||||
|
|
||||||
|
@ -201,15 +220,15 @@
|
||||||
parent: id,
|
parent: id,
|
||||||
index: idx,
|
index: idx,
|
||||||
})
|
})
|
||||||
}
|
}, ThrottleRate)
|
||||||
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
|
|
||||||
|
|
||||||
const handleEvent = e => {
|
const handleEvent = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
throttledProcessEvent(e.clientX, e.clientY)
|
e.stopPropagation()
|
||||||
|
processEvent(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when on top of a component
|
// Callback when on top of a component.
|
||||||
const onDragOver = e => {
|
const onDragOver = e => {
|
||||||
if (!source || !target) {
|
if (!source || !target) {
|
||||||
return
|
return
|
||||||
|
@ -241,18 +260,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when dropping a drag on top of some component
|
// Callback when dropping a drag on top of some component
|
||||||
const onDrop = () => {
|
const onDrop = async () => {
|
||||||
if (!source || !drop?.parent || drop?.index == null) {
|
if (!source || !drop?.parent || drop?.index == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're adding a new component rather than moving one
|
// Check if we're adding a new component rather than moving one
|
||||||
if (source.newComponentType) {
|
if (source.newComponentType) {
|
||||||
builderStore.actions.dropNewComponent(
|
dropping = true
|
||||||
|
await builderStore.actions.dropNewComponent(
|
||||||
source.newComponentType,
|
source.newComponentType,
|
||||||
drop.parent,
|
drop.parent,
|
||||||
drop.index
|
drop.index
|
||||||
)
|
)
|
||||||
|
dropping = false
|
||||||
|
stopDragging()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,11 +311,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (legacyDropTarget && legacyDropMode) {
|
if (legacyDropTarget && legacyDropMode) {
|
||||||
builderStore.actions.moveComponent(
|
dropping = true
|
||||||
|
await builderStore.actions.moveComponent(
|
||||||
source.id,
|
source.id,
|
||||||
legacyDropTarget,
|
legacyDropTarget,
|
||||||
legacyDropMode
|
legacyDropMode
|
||||||
)
|
)
|
||||||
|
dropping = false
|
||||||
|
stopDragging()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
<script>
|
|
||||||
import { dndBounds } from "stores"
|
|
||||||
import { DNDPlaceholderID } from "constants"
|
|
||||||
|
|
||||||
$: style = getStyle($dndBounds)
|
|
||||||
|
|
||||||
const getStyle = bounds => {
|
|
||||||
if (!bounds) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `--height: ${bounds.height}px; --width: ${bounds.width}px;`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if style}
|
|
||||||
<div class="wrapper">
|
|
||||||
<div class="placeholder" id={DNDPlaceholderID} {style} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
display: block;
|
|
||||||
height: var(--height);
|
|
||||||
width: var(--width);
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -6,7 +6,8 @@
|
||||||
let left, top, height, width
|
let left, top, height, width
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
const node = document.getElementById(DNDPlaceholderID)
|
const node =
|
||||||
|
document.getElementsByClassName(DNDPlaceholderID)[0]?.childNodes[0]
|
||||||
if (!node) {
|
if (!node) {
|
||||||
height = 0
|
height = 0
|
||||||
width = 0
|
width = 0
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { builderStore, componentStore } from "stores"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
let dragInfo
|
||||||
|
let gridStyles
|
||||||
|
let id
|
||||||
|
|
||||||
|
// Some memoisation of primitive types for performance
|
||||||
|
$: jsonStyles = JSON.stringify(gridStyles)
|
||||||
|
$: id = dragInfo?.id || id
|
||||||
|
|
||||||
|
// Set ephemeral grid styles on the dragged component
|
||||||
|
$: componentStore.actions.getComponentInstance(id)?.setEphemeralStyles({
|
||||||
|
...gridStyles,
|
||||||
|
...(gridStyles ? { "z-index": 999 } : null),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Util to check if a DND event originates from a grid (or inside a grid).
|
||||||
|
// This is important as we do not handle grid DND in this handler.
|
||||||
|
const isGridEvent = e => {
|
||||||
|
return (
|
||||||
|
e.target
|
||||||
|
.closest?.(".component")
|
||||||
|
?.parentNode.closest(".component")
|
||||||
|
?.childNodes[0].classList.contains("grid") ||
|
||||||
|
e.target.classList.contains("anchor")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util to get the inner DOM node by a component ID
|
||||||
|
const getDOMNode = id => {
|
||||||
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
return [...component.children][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
||||||
|
if (!dragInfo?.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mode, side, gridId, grid } = dragInfo
|
||||||
|
const {
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
rowStart,
|
||||||
|
rowEnd,
|
||||||
|
colStart,
|
||||||
|
colEnd,
|
||||||
|
rowDeltaMin,
|
||||||
|
rowDeltaMax,
|
||||||
|
colDeltaMin,
|
||||||
|
colDeltaMax,
|
||||||
|
} = grid
|
||||||
|
|
||||||
|
const domGrid = getDOMNode(gridId)
|
||||||
|
const cols = parseInt(domGrid.dataset.cols)
|
||||||
|
const rows = parseInt(domGrid.dataset.rows)
|
||||||
|
const { width, height } = domGrid.getBoundingClientRect()
|
||||||
|
|
||||||
|
const colWidth = width / cols
|
||||||
|
const diffX = mouseX - startX
|
||||||
|
let deltaX = Math.round(diffX / colWidth)
|
||||||
|
const rowHeight = height / rows
|
||||||
|
const diffY = mouseY - startY
|
||||||
|
let deltaY = Math.round(diffY / rowHeight)
|
||||||
|
|
||||||
|
if (mode === "move") {
|
||||||
|
deltaY = Math.min(Math.max(deltaY, rowDeltaMin), rowDeltaMax)
|
||||||
|
deltaX = Math.min(Math.max(deltaX, colDeltaMin), colDeltaMax)
|
||||||
|
const newStyles = {
|
||||||
|
"grid-row-start": rowStart + deltaY,
|
||||||
|
"grid-row-end": rowEnd + deltaY,
|
||||||
|
"grid-column-start": colStart + deltaX,
|
||||||
|
"grid-column-end": colEnd + deltaX,
|
||||||
|
}
|
||||||
|
if (JSON.stringify(newStyles) !== jsonStyles) {
|
||||||
|
gridStyles = newStyles
|
||||||
|
}
|
||||||
|
} else if (mode === "resize") {
|
||||||
|
let newStyles = {}
|
||||||
|
if (side === "right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
} else if (side === "left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
} else if (side === "top") {
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
} else if (side === "bottom") {
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "bottom-right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "bottom-left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
|
||||||
|
} else if (side === "top-right") {
|
||||||
|
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
} else if (side === "top-left") {
|
||||||
|
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
|
||||||
|
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||||
|
}
|
||||||
|
if (JSON.stringify(newStyles) !== jsonStyles) {
|
||||||
|
gridStyles = newStyles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
const handleEvent = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
processEvent(e.clientX, e.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when initially starting a drag on a draggable component
|
||||||
|
const onDragStart = e => {
|
||||||
|
if (!isGridEvent(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide drag ghost image
|
||||||
|
e.dataTransfer.setDragImage(new Image(), 0, 0)
|
||||||
|
|
||||||
|
// Extract state
|
||||||
|
let mode, id, side
|
||||||
|
if (e.target.classList.contains("anchor")) {
|
||||||
|
// Handle resize
|
||||||
|
mode = "resize"
|
||||||
|
id = e.target.dataset.id
|
||||||
|
side = e.target.dataset.side
|
||||||
|
} else {
|
||||||
|
// Handle move
|
||||||
|
mode = "move"
|
||||||
|
const component = e.target.closest(".component")
|
||||||
|
id = component.dataset.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find grid parent
|
||||||
|
const domComponent = getDOMNode(id)
|
||||||
|
const gridId = domComponent?.closest(".grid")?.parentNode.dataset.id
|
||||||
|
if (!gridId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
domTarget: e.target,
|
||||||
|
id,
|
||||||
|
gridId,
|
||||||
|
mode,
|
||||||
|
side,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event handler to clear all drag state when dragging ends
|
||||||
|
dragInfo.domTarget.addEventListener("dragend", stopDragging)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when entering a potential drop target
|
||||||
|
const onDragEnter = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo || dragInfo.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const domGrid = getDOMNode(dragInfo.gridId)
|
||||||
|
const gridCols = parseInt(domGrid.dataset.cols)
|
||||||
|
const gridRows = parseInt(domGrid.dataset.rows)
|
||||||
|
const domNode = getDOMNode(dragInfo.id)
|
||||||
|
const styles = window.getComputedStyle(domNode)
|
||||||
|
if (domGrid) {
|
||||||
|
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
|
||||||
|
const getStyle = x => parseInt(styles?.[x] || "0")
|
||||||
|
const getColStyle = x => minMax(getStyle(x), 1, gridCols + 1)
|
||||||
|
const getRowStyle = x => minMax(getStyle(x), 1, gridRows + 1)
|
||||||
|
dragInfo.grid = {
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
rowStart: getRowStyle("grid-row-start"),
|
||||||
|
rowEnd: getRowStyle("grid-row-end"),
|
||||||
|
colStart: getColStyle("grid-column-start"),
|
||||||
|
colEnd: getColStyle("grid-column-end"),
|
||||||
|
rowDeltaMin: 1 - getRowStyle("grid-row-start"),
|
||||||
|
rowDeltaMax: gridRows + 1 - getRowStyle("grid-row-end"),
|
||||||
|
colDeltaMin: 1 - getColStyle("grid-column-start"),
|
||||||
|
colDeltaMax: gridCols + 1 - getColStyle("grid-column-end"),
|
||||||
|
}
|
||||||
|
handleEvent(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragOver = e => {
|
||||||
|
if (!dragInfo?.grid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when drag stops (whether dropped or not)
|
||||||
|
const stopDragging = async () => {
|
||||||
|
// Save changes
|
||||||
|
if (gridStyles) {
|
||||||
|
await builderStore.actions.updateStyles(gridStyles, dragInfo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset listener
|
||||||
|
if (dragInfo?.domTarget) {
|
||||||
|
dragInfo.domTarget.removeEventListener("dragend", stopDragging)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
dragInfo = null
|
||||||
|
gridStyles = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
|
document.addEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
|
document.removeEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -4,11 +4,25 @@
|
||||||
import { builderStore, dndIsDragging } from "stores"
|
import { builderStore, dndIsDragging } from "stores"
|
||||||
|
|
||||||
let componentId
|
let componentId
|
||||||
|
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest(".interactive.component")
|
// Ignore if dragging
|
||||||
const newId = element?.dataset?.id
|
if (e.buttons > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let newId
|
||||||
|
if (e.target.classList.contains("anchor")) {
|
||||||
|
// Handle resize anchors
|
||||||
|
newId = e.target.dataset.id
|
||||||
|
} else {
|
||||||
|
// Handle normal components
|
||||||
|
const element = e.target.closest(".interactive.component")
|
||||||
|
newId = element?.dataset?.id
|
||||||
|
}
|
||||||
|
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
}
|
}
|
||||||
|
@ -34,4 +48,5 @@
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
transition
|
transition
|
||||||
{zIndex}
|
{zIndex}
|
||||||
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,9 +10,22 @@
|
||||||
export let icon
|
export let icon
|
||||||
export let color
|
export let color
|
||||||
export let zIndex
|
export let zIndex
|
||||||
|
export let componentId
|
||||||
export let transition = false
|
export let transition = false
|
||||||
export let line = false
|
export let line = false
|
||||||
export let alignRight = false
|
export let alignRight = false
|
||||||
|
export let showResizeAnchors = false
|
||||||
|
|
||||||
|
const AnchorSides = [
|
||||||
|
"right",
|
||||||
|
"left",
|
||||||
|
"top",
|
||||||
|
"bottom",
|
||||||
|
"bottom-right",
|
||||||
|
"bottom-left",
|
||||||
|
"top-right",
|
||||||
|
"top-left",
|
||||||
|
]
|
||||||
|
|
||||||
$: flipped = top < 24
|
$: flipped = top < 24
|
||||||
</script>
|
</script>
|
||||||
|
@ -40,6 +53,18 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showResizeAnchors}
|
||||||
|
{#each AnchorSides as side}
|
||||||
|
<div
|
||||||
|
draggable="true"
|
||||||
|
class="anchor {side}"
|
||||||
|
data-side={side}
|
||||||
|
data-id={componentId}
|
||||||
|
>
|
||||||
|
<div class="anchor-inner" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -105,4 +130,64 @@
|
||||||
/* Icon styles */
|
/* Icon styles */
|
||||||
.label :global(.spectrum-Icon + .text) {
|
.label :global(.spectrum-Icon + .text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Anchor */
|
||||||
|
.anchor {
|
||||||
|
--size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
pointer-events: all;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.anchor-inner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid var(--color);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.anchor.right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
cursor: e-resize;
|
||||||
|
}
|
||||||
|
.anchor.left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(50% - var(--size) / 2);
|
||||||
|
cursor: w-resize;
|
||||||
|
}
|
||||||
|
.anchor.bottom {
|
||||||
|
left: calc(50% - var(--size) / 2 + 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: s-resize;
|
||||||
|
}
|
||||||
|
.anchor.top {
|
||||||
|
left: calc(50% - var(--size) / 2 + 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: n-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor.bottom-right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: se-resize;
|
||||||
|
}
|
||||||
|
.anchor.bottom-left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
bottom: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: sw-resize;
|
||||||
|
}
|
||||||
|
.anchor.top-right {
|
||||||
|
right: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
.anchor.top-left {
|
||||||
|
left: calc(var(--size) / -2 - 1px);
|
||||||
|
top: calc(var(--size) / -2 - 1px);
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,11 +9,13 @@
|
||||||
export let transition
|
export let transition
|
||||||
export let zIndex
|
export let zIndex
|
||||||
export let prefix = null
|
export let prefix = null
|
||||||
|
export let allowResizeAnchors = false
|
||||||
|
|
||||||
let indicators = []
|
let indicators = []
|
||||||
let interval
|
let interval
|
||||||
let text
|
let text
|
||||||
let icon
|
let icon
|
||||||
|
let insideGrid = false
|
||||||
|
|
||||||
$: visibleIndicators = indicators.filter(x => x.visible)
|
$: visibleIndicators = indicators.filter(x => x.visible)
|
||||||
$: offset = $builderStore.inBuilder ? 0 : 2
|
$: offset = $builderStore.inBuilder ? 0 : 2
|
||||||
|
@ -23,6 +25,20 @@
|
||||||
let callbackCount = 0
|
let callbackCount = 0
|
||||||
let nextIndicators = []
|
let nextIndicators = []
|
||||||
|
|
||||||
|
const checkInsideGrid = id => {
|
||||||
|
const component = document.getElementsByClassName(id)[0]
|
||||||
|
const domNode = component?.children[0]
|
||||||
|
|
||||||
|
// Ignore grid itself
|
||||||
|
if (domNode?.classList.contains("grid")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return component?.parentNode
|
||||||
|
?.closest?.(".component")
|
||||||
|
?.childNodes[0]?.classList.contains("grid")
|
||||||
|
}
|
||||||
|
|
||||||
const createIntersectionCallback = idx => entries => {
|
const createIntersectionCallback = idx => entries => {
|
||||||
if (callbackCount >= observers.length) {
|
if (callbackCount >= observers.length) {
|
||||||
return
|
return
|
||||||
|
@ -52,6 +68,11 @@
|
||||||
observers = []
|
observers = []
|
||||||
nextIndicators = []
|
nextIndicators = []
|
||||||
|
|
||||||
|
// Check if we're inside a grid
|
||||||
|
if (allowResizeAnchors) {
|
||||||
|
insideGrid = checkInsideGrid(componentId)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine next set of indicators
|
// Determine next set of indicators
|
||||||
const parents = document.getElementsByClassName(componentId)
|
const parents = document.getElementsByClassName(componentId)
|
||||||
if (parents.length) {
|
if (parents.length) {
|
||||||
|
@ -127,6 +148,8 @@
|
||||||
height={indicator.height}
|
height={indicator.height}
|
||||||
text={idx === 0 ? text : null}
|
text={idx === 0 ? text : null}
|
||||||
icon={idx === 0 ? icon : null}
|
icon={idx === 0 ? icon : null}
|
||||||
|
showResizeAnchors={allowResizeAnchors && insideGrid}
|
||||||
|
{componentId}
|
||||||
{transition}
|
{transition}
|
||||||
{zIndex}
|
{zIndex}
|
||||||
{color}
|
{color}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { builderStore, dndIsDragging } from "stores"
|
import { builderStore } from "stores"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
|
||||||
$: color = $builderStore.editMode
|
$: color = $builderStore.editMode
|
||||||
|
@ -8,8 +8,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={$dndIsDragging ? null : $builderStore.selectedComponentId}
|
componentId={$builderStore.selectedComponentId}
|
||||||
{color}
|
{color}
|
||||||
zIndex="910"
|
zIndex="910"
|
||||||
transition
|
transition
|
||||||
|
allowResizeAnchors
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
|
|
||||||
$: definition = $componentStore.selectedComponentDefinition
|
$: definition = $componentStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar && !$dndIsDragging
|
$: showBar = definition?.showSettingsBar && !$dndIsDragging
|
||||||
|
$: {
|
||||||
|
if (!showBar) {
|
||||||
|
measured = false
|
||||||
|
}
|
||||||
|
}
|
||||||
$: settings = getBarSettings(definition)
|
$: settings = getBarSettings(definition)
|
||||||
|
|
||||||
const getBarSettings = definition => {
|
const getBarSettings = definition => {
|
||||||
|
|
|
@ -32,5 +32,4 @@ export const ActionTypes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DNDPlaceholderID = "dnd-placeholder"
|
export const DNDPlaceholderID = "dnd-placeholder"
|
||||||
export const DNDPlaceholderType = "dnd-placeholder"
|
|
||||||
export const ScreenslotType = "screenslot"
|
export const ScreenslotType = "screenslot"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
componentStore,
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
dndStore,
|
dndStore,
|
||||||
|
eventStore,
|
||||||
} from "./stores"
|
} from "./stores"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
@ -46,7 +47,9 @@ const loadBudibase = async () => {
|
||||||
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
||||||
|
|
||||||
// Fetch environment info
|
// Fetch environment info
|
||||||
await environmentStore.actions.fetchEnvironment()
|
if (!get(environmentStore)?.loaded) {
|
||||||
|
await environmentStore.actions.fetchEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
// Enable dev tools or not. We need to be using a dev app and not inside
|
// Enable dev tools or not. We need to be using a dev app and not inside
|
||||||
// the builder preview to enable them.
|
// the builder preview to enable them.
|
||||||
|
@ -54,15 +57,17 @@ const loadBudibase = async () => {
|
||||||
devToolsStore.actions.setEnabled(enableDevTools)
|
devToolsStore.actions.setEnabled(enableDevTools)
|
||||||
|
|
||||||
// Register handler for runtime events from the builder
|
// Register handler for runtime events from the builder
|
||||||
window.handleBuilderRuntimeEvent = (name, payload) => {
|
window.handleBuilderRuntimeEvent = (type, data) => {
|
||||||
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (name === "eject-block") {
|
if (type === "event-completed") {
|
||||||
const block = blockStore.actions.getBlock(payload)
|
eventStore.actions.resolveEvent(data)
|
||||||
|
} else if (type === "eject-block") {
|
||||||
|
const block = blockStore.actions.getBlock(data)
|
||||||
block?.eject()
|
block?.eject()
|
||||||
} else if (name === "dragging-new-component") {
|
} else if (type === "dragging-new-component") {
|
||||||
const { dragging, component } = payload
|
const { dragging, component } = data
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
const definition =
|
const definition =
|
||||||
componentStore.actions.getComponentDefinition(component)
|
componentStore.actions.getComponentDefinition(component)
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { devToolsStore } from "./devTools.js"
|
import { devToolsStore } from "./devTools.js"
|
||||||
|
import { eventStore } from "./events.js"
|
||||||
const dispatchEvent = (type, data = {}) => {
|
|
||||||
window.parent.postMessage({ type, data })
|
|
||||||
}
|
|
||||||
|
|
||||||
const createBuilderStore = () => {
|
const createBuilderStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -19,6 +16,7 @@ const createBuilderStore = () => {
|
||||||
navigation: null,
|
navigation: null,
|
||||||
hiddenComponentIds: [],
|
hiddenComponentIds: [],
|
||||||
usedPlugins: null,
|
usedPlugins: null,
|
||||||
|
eventResolvers: {},
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
// Legacy - allow the builder to specify a layout
|
||||||
layout: null,
|
layout: null,
|
||||||
|
@ -35,22 +33,25 @@ const createBuilderStore = () => {
|
||||||
selectedComponentId: id,
|
selectedComponentId: id,
|
||||||
}))
|
}))
|
||||||
devToolsStore.actions.setAllowSelection(false)
|
devToolsStore.actions.setAllowSelection(false)
|
||||||
dispatchEvent("select-component", { id })
|
eventStore.actions.dispatchEvent("select-component", { id })
|
||||||
},
|
},
|
||||||
updateProp: (prop, value) => {
|
updateProp: (prop, value) => {
|
||||||
dispatchEvent("update-prop", { prop, value })
|
eventStore.actions.dispatchEvent("update-prop", { prop, value })
|
||||||
|
},
|
||||||
|
updateStyles: async (styles, id) => {
|
||||||
|
await eventStore.actions.dispatchEvent("update-styles", { styles, id })
|
||||||
},
|
},
|
||||||
keyDown: (key, ctrlKey) => {
|
keyDown: (key, ctrlKey) => {
|
||||||
dispatchEvent("key-down", { key, ctrlKey })
|
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })
|
||||||
},
|
},
|
||||||
duplicateComponent: id => {
|
duplicateComponent: id => {
|
||||||
dispatchEvent("duplicate-component", { id })
|
eventStore.actions.dispatchEvent("duplicate-component", { id })
|
||||||
},
|
},
|
||||||
deleteComponent: id => {
|
deleteComponent: id => {
|
||||||
dispatchEvent("delete-component", { id })
|
eventStore.actions.dispatchEvent("delete-component", { id })
|
||||||
},
|
},
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
dispatchEvent("preview-loaded")
|
eventStore.actions.dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
analyticsPing: async () => {
|
analyticsPing: async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -59,15 +60,15 @@ const createBuilderStore = () => {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moveComponent: (componentId, destinationComponentId, mode) => {
|
moveComponent: async (componentId, destinationComponentId, mode) => {
|
||||||
dispatchEvent("move-component", {
|
await eventStore.actions.dispatchEvent("move-component", {
|
||||||
componentId,
|
componentId,
|
||||||
destinationComponentId,
|
destinationComponentId,
|
||||||
mode,
|
mode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dropNewComponent: (component, parent, index) => {
|
dropNewComponent: (component, parent, index) => {
|
||||||
dispatchEvent("drop-new-component", {
|
eventStore.actions.dispatchEvent("drop-new-component", {
|
||||||
component,
|
component,
|
||||||
parent,
|
parent,
|
||||||
index,
|
index,
|
||||||
|
@ -80,16 +81,16 @@ const createBuilderStore = () => {
|
||||||
store.update(state => ({ ...state, editMode: enabled }))
|
store.update(state => ({ ...state, editMode: enabled }))
|
||||||
},
|
},
|
||||||
clickNav: () => {
|
clickNav: () => {
|
||||||
dispatchEvent("click-nav")
|
eventStore.actions.dispatchEvent("click-nav")
|
||||||
},
|
},
|
||||||
requestAddComponent: () => {
|
requestAddComponent: () => {
|
||||||
dispatchEvent("request-add-component")
|
eventStore.actions.dispatchEvent("request-add-component")
|
||||||
},
|
},
|
||||||
highlightSetting: setting => {
|
highlightSetting: setting => {
|
||||||
dispatchEvent("highlight-setting", { setting })
|
eventStore.actions.dispatchEvent("highlight-setting", { setting })
|
||||||
},
|
},
|
||||||
ejectBlock: (id, definition) => {
|
ejectBlock: (id, definition) => {
|
||||||
dispatchEvent("eject-block", { id, definition })
|
eventStore.actions.dispatchEvent("eject-block", { id, definition })
|
||||||
},
|
},
|
||||||
updateUsedPlugin: (name, hash) => {
|
updateUsedPlugin: (name, hash) => {
|
||||||
// Check if we used this plugin
|
// Check if we used this plugin
|
||||||
|
@ -106,7 +107,7 @@ const createBuilderStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the builder so we can reload component definitions
|
// Notify the builder so we can reload component definitions
|
||||||
dispatchEvent("reload-plugin")
|
eventStore.actions.dispatchEvent("reload-plugin")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,9 +5,8 @@ import { devToolsStore } from "./devTools"
|
||||||
import { screenStore } from "./screens"
|
import { screenStore } from "./screens"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import Router from "../components/Router.svelte"
|
import Router from "../components/Router.svelte"
|
||||||
import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte"
|
|
||||||
import * as AppComponents from "../components/app/index.js"
|
import * as AppComponents from "../components/app/index.js"
|
||||||
import { DNDPlaceholderType, ScreenslotType } from "../constants.js"
|
import { ScreenslotType } from "../constants.js"
|
||||||
|
|
||||||
const budibasePrefix = "@budibase/standard-components/"
|
const budibasePrefix = "@budibase/standard-components/"
|
||||||
|
|
||||||
|
@ -49,6 +48,9 @@ const createComponentStore = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const registerInstance = (id, instance) => {
|
const registerInstance = (id, instance) => {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// If this is a custom component, flag it so we can reload this component
|
// If this is a custom component, flag it so we can reload this component
|
||||||
// later if required
|
// later if required
|
||||||
|
@ -68,6 +70,9 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterInstance = id => {
|
const unregisterInstance = id => {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
// Remove from custom component map if required
|
// Remove from custom component map if required
|
||||||
const component = state.mountedComponents[id]?.instance?.component
|
const component = state.mountedComponents[id]?.instance?.component
|
||||||
|
@ -103,8 +108,6 @@ const createComponentStore = () => {
|
||||||
// Screenslot is an edge case
|
// Screenslot is an edge case
|
||||||
if (type === ScreenslotType) {
|
if (type === ScreenslotType) {
|
||||||
type = `${budibasePrefix}${type}`
|
type = `${budibasePrefix}${type}`
|
||||||
} else if (type === DNDPlaceholderType) {
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle built-in components
|
// Handle built-in components
|
||||||
|
@ -124,8 +127,6 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
if (type === ScreenslotType) {
|
if (type === ScreenslotType) {
|
||||||
return Router
|
return Router
|
||||||
} else if (type === DNDPlaceholderType) {
|
|
||||||
return DNDPlaceholder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle budibase components
|
// Handle budibase components
|
||||||
|
@ -140,6 +141,13 @@ const createComponentStore = () => {
|
||||||
return customComponentManifest?.[type]?.Component
|
return customComponentManifest?.[type]?.Component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getComponentInstance = id => {
|
||||||
|
if (!id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return get(store).mountedComponents[id]
|
||||||
|
}
|
||||||
|
|
||||||
const registerCustomComponent = ({ Component, schema, version }) => {
|
const registerCustomComponent = ({ Component, schema, version }) => {
|
||||||
if (!Component || !schema?.schema?.name || !version) {
|
if (!Component || !schema?.schema?.name || !version) {
|
||||||
return
|
return
|
||||||
|
@ -171,6 +179,7 @@ const createComponentStore = () => {
|
||||||
getComponentById,
|
getComponentById,
|
||||||
getComponentDefinition,
|
getComponentDefinition,
|
||||||
getComponentConstructor,
|
getComponentConstructor,
|
||||||
|
getComponentInstance,
|
||||||
registerCustomComponent,
|
registerCustomComponent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import { computed } from "../utils/computed.js"
|
||||||
|
|
||||||
const createDndStore = () => {
|
const createDndStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -77,14 +78,11 @@ export const dndStore = createDndStore()
|
||||||
// performance by deriving any state that needs to be externally observed.
|
// performance by deriving any state that needs to be externally observed.
|
||||||
// By doing this and using primitives, we can avoid invalidating other stores
|
// By doing this and using primitives, we can avoid invalidating other stores
|
||||||
// or components which depend on DND state unless values actually change.
|
// or components which depend on DND state unless values actually change.
|
||||||
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)
|
export const dndParent = computed(dndStore, x => x.drop?.parent)
|
||||||
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
|
export const dndIndex = computed(dndStore, x => x.drop?.index)
|
||||||
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
|
export const dndBounds = computed(dndStore, x => x.source?.bounds)
|
||||||
export const dndBounds = derived(
|
export const dndIsDragging = computed(dndStore, x => !!x.source)
|
||||||
|
export const dndIsNewComponent = computed(
|
||||||
dndStore,
|
dndStore,
|
||||||
$dndStore => $dndStore.source?.bounds
|
x => x.source?.newComponentType != null
|
||||||
)
|
|
||||||
export const dndIsNewComponent = derived(
|
|
||||||
dndStore,
|
|
||||||
$dndStore => $dndStore.source?.newComponentType != null
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { API } from "api"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
loaded: false,
|
||||||
cloud: false,
|
cloud: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ const createEnvironmentStore = () => {
|
||||||
store.set({
|
store.set({
|
||||||
...initialState,
|
...initialState,
|
||||||
...environment,
|
...environment,
|
||||||
|
loaded: true,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
store.set(initialState)
|
store.set(initialState)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
const createEventStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
eventResolvers: {},
|
||||||
|
}
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
dispatchEvent: (type, data) => {
|
||||||
|
const id = Math.random()
|
||||||
|
return new Promise(resolve => {
|
||||||
|
window.parent.postMessage({ type, data, id })
|
||||||
|
store.update(state => {
|
||||||
|
state.eventResolvers[id] = resolve
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resolveEvent: data => {
|
||||||
|
get(store).eventResolvers[data]?.()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventStore = createEventStore()
|
|
@ -15,6 +15,7 @@ export { uploadStore } from "./uploads.js"
|
||||||
export { rowSelectionStore } from "./rowSelection.js"
|
export { rowSelectionStore } from "./rowSelection.js"
|
||||||
export { blockStore } from "./blocks.js"
|
export { blockStore } from "./blocks.js"
|
||||||
export { environmentStore } from "./environment"
|
export { environmentStore } from "./environment"
|
||||||
|
export { eventStore } from "./events.js"
|
||||||
export {
|
export {
|
||||||
dndStore,
|
dndStore,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { derived } from "svelte/store"
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { dndIndex, dndParent, dndIsNewComponent } from "./dnd.js"
|
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { findComponentById, findComponentParent } from "../utils/components.js"
|
import { findComponentById, findComponentParent } from "../utils/components.js"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
|
import { DNDPlaceholderID } from "constants"
|
||||||
|
|
||||||
const createScreenStore = () => {
|
const createScreenStore = () => {
|
||||||
const store = derived(
|
const store = derived(
|
||||||
|
@ -17,6 +17,7 @@ const createScreenStore = () => {
|
||||||
dndParent,
|
dndParent,
|
||||||
dndIndex,
|
dndIndex,
|
||||||
dndIsNewComponent,
|
dndIsNewComponent,
|
||||||
|
dndBounds,
|
||||||
],
|
],
|
||||||
([
|
([
|
||||||
$appStore,
|
$appStore,
|
||||||
|
@ -25,6 +26,7 @@ const createScreenStore = () => {
|
||||||
$dndParent,
|
$dndParent,
|
||||||
$dndIndex,
|
$dndIndex,
|
||||||
$dndIsNewComponent,
|
$dndIsNewComponent,
|
||||||
|
$dndBounds,
|
||||||
]) => {
|
]) => {
|
||||||
let activeLayout, activeScreen
|
let activeLayout, activeScreen
|
||||||
let screens
|
let screens
|
||||||
|
@ -62,32 +64,43 @@ const createScreenStore = () => {
|
||||||
|
|
||||||
// Insert DND placeholder if required
|
// Insert DND placeholder if required
|
||||||
if (activeScreen && $dndParent && $dndIndex != null) {
|
if (activeScreen && $dndParent && $dndIndex != null) {
|
||||||
|
const { selectedComponentId } = $builderStore
|
||||||
|
|
||||||
|
// Extract and save the selected component as we need a reference to it
|
||||||
|
// later, and we may be removing it
|
||||||
|
let selectedParent = findComponentParent(
|
||||||
|
activeScreen.props,
|
||||||
|
selectedComponentId
|
||||||
|
)
|
||||||
|
|
||||||
// Remove selected component from tree if we are moving an existing
|
// Remove selected component from tree if we are moving an existing
|
||||||
// component
|
// component
|
||||||
const { selectedComponentId } = $builderStore
|
if (!$dndIsNewComponent && selectedParent) {
|
||||||
if (!$dndIsNewComponent) {
|
selectedParent._children = selectedParent._children?.filter(
|
||||||
let selectedParent = findComponentParent(
|
x => x._id !== selectedComponentId
|
||||||
activeScreen.props,
|
|
||||||
selectedComponentId
|
|
||||||
)
|
)
|
||||||
if (selectedParent) {
|
|
||||||
selectedParent._children = selectedParent._children?.filter(
|
|
||||||
x => x._id !== selectedComponentId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert placeholder component
|
// Insert placeholder component
|
||||||
const placeholder = {
|
const componentToInsert = {
|
||||||
_component: DNDPlaceholderID,
|
_component: "@budibase/standard-components/container",
|
||||||
_id: DNDPlaceholderType,
|
_id: DNDPlaceholderID,
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
width: `${$dndBounds?.width || 400}px`,
|
||||||
|
height: `${$dndBounds?.height || 200}px`,
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
static: true,
|
static: true,
|
||||||
}
|
}
|
||||||
let parent = findComponentById(activeScreen.props, $dndParent)
|
let parent = findComponentById(activeScreen.props, $dndParent)
|
||||||
if (!parent._children?.length) {
|
if (parent) {
|
||||||
parent._children = [placeholder]
|
if (!parent._children?.length) {
|
||||||
} else {
|
parent._children = [componentToInsert]
|
||||||
parent._children.splice($dndIndex, 0, placeholder)
|
} else {
|
||||||
|
parent._children.splice($dndIndex, 0, componentToInsert)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of Svelte's built in "derived" stores, which the addition of deep
|
||||||
|
* comparison of non-primitives. Falls back to using shallow comparison for
|
||||||
|
* primitive types to avoid performance penalties.
|
||||||
|
* Useful for instances where a deep comparison is cheaper than an additional
|
||||||
|
* store invalidation.
|
||||||
|
* @param store the store to observer
|
||||||
|
* @param deriveValue the derivation function
|
||||||
|
* @returns {Writable<*>} a derived svelte store containing just the derived value
|
||||||
|
*/
|
||||||
|
export const computed = (store, deriveValue) => {
|
||||||
|
const initialValue = deriveValue(store)
|
||||||
|
const computedStore = writable(initialValue)
|
||||||
|
let lastKey = getKey(initialValue)
|
||||||
|
|
||||||
|
store.subscribe(state => {
|
||||||
|
const value = deriveValue(state)
|
||||||
|
const key = getKey(value)
|
||||||
|
if (key !== lastKey) {
|
||||||
|
lastKey = key
|
||||||
|
computedStore.set(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return computedStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to serialise any value into a primitive which can be cheaply
|
||||||
|
// and shallowly compared
|
||||||
|
const getKey = value => {
|
||||||
|
if (value == null || typeof value !== "object") {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,17 +6,29 @@
|
||||||
*/
|
*/
|
||||||
export const sequential = fn => {
|
export const sequential = fn => {
|
||||||
let queue = []
|
let queue = []
|
||||||
return async (...params) => {
|
return (...params) => {
|
||||||
queue.push(async () => {
|
return new Promise((resolve, reject) => {
|
||||||
await fn(...params)
|
queue.push(async () => {
|
||||||
queue.shift()
|
let data, error
|
||||||
if (queue.length) {
|
try {
|
||||||
await queue[0]()
|
data = await fn(...params)
|
||||||
|
} catch (err) {
|
||||||
|
error = err
|
||||||
|
}
|
||||||
|
queue.shift()
|
||||||
|
if (queue.length) {
|
||||||
|
queue[0]()
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (queue.length === 1) {
|
||||||
|
queue[0]()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (queue.length === 1) {
|
|
||||||
await queue[0]()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue