2019-08-20 08:18:23 +12:00
|
|
|
<script>
|
2021-09-16 18:28:59 +12:00
|
|
|
import { get } from "svelte/store"
|
2021-07-27 09:53:11 +12:00
|
|
|
import { onMount, onDestroy } from "svelte"
|
2021-06-16 06:36:56 +12:00
|
|
|
import { store, currentAsset } from "builderStore"
|
2020-05-07 21:53:34 +12:00
|
|
|
import iframeTemplate from "./iframeTemplate"
|
2020-11-24 00:29:24 +13:00
|
|
|
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
2021-05-07 03:39:34 +12:00
|
|
|
import { FrontendTypes } from "constants"
|
2021-06-24 01:21:37 +12:00
|
|
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
2021-10-05 04:50:52 +13:00
|
|
|
import {
|
|
|
|
ProgressCircle,
|
|
|
|
Layout,
|
|
|
|
Heading,
|
|
|
|
Body,
|
|
|
|
notifications,
|
|
|
|
} from "@budibase/bbui"
|
2021-07-01 06:37:03 +12:00
|
|
|
import ErrorSVG from "assets/error.svg?raw"
|
2021-09-16 18:35:19 +12:00
|
|
|
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
2019-09-03 21:42:19 +12:00
|
|
|
|
2020-02-12 05:36:16 +13:00
|
|
|
let iframe
|
2020-12-03 07:10:46 +13:00
|
|
|
let layout
|
|
|
|
let screen
|
2021-06-24 01:21:37 +12:00
|
|
|
let confirmDeleteDialog
|
|
|
|
let idToDelete
|
2021-07-01 06:37:03 +12:00
|
|
|
let loading = true
|
|
|
|
let error
|
2020-05-26 02:23:56 +12:00
|
|
|
|
2020-11-24 03:27:45 +13:00
|
|
|
// Create screen slot placeholder for use when a page is selected rather
|
|
|
|
// than a screen
|
2020-11-20 05:55:59 +13:00
|
|
|
const screenPlaceholder = new Screen()
|
|
|
|
.name("Screen Placeholder")
|
|
|
|
.route("*")
|
2021-01-13 09:00:35 +13:00
|
|
|
.component("@budibase/standard-components/screenslot")
|
2020-11-20 05:55:59 +13:00
|
|
|
.instanceName("Content Placeholder")
|
|
|
|
.json()
|
2020-06-09 08:13:19 +12:00
|
|
|
|
2021-04-02 02:10:49 +13:00
|
|
|
// Construct iframe template
|
|
|
|
$: template = iframeTemplate.replace(
|
|
|
|
/\{\{ CLIENT_LIB_PATH }}/,
|
|
|
|
$store.clientLibPath
|
|
|
|
)
|
|
|
|
|
2020-11-24 00:29:24 +13:00
|
|
|
// Extract data to pass to the iframe
|
2020-12-03 07:10:46 +13:00
|
|
|
$: {
|
|
|
|
if ($store.currentFrontEndType === FrontendTypes.LAYOUT) {
|
|
|
|
layout = $currentAsset
|
|
|
|
screen = screenPlaceholder
|
|
|
|
} else {
|
|
|
|
screen = $currentAsset
|
2020-12-08 04:49:13 +13:00
|
|
|
layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
|
2020-12-03 07:10:46 +13:00
|
|
|
}
|
|
|
|
}
|
2020-12-08 04:27:46 +13:00
|
|
|
$: selectedComponentId = $store.selectedComponentId ?? ""
|
2020-11-24 00:29:24 +13:00
|
|
|
$: previewData = {
|
2021-01-08 03:53:56 +13:00
|
|
|
appId: $store.appId,
|
2020-11-26 06:56:09 +13:00
|
|
|
layout,
|
2020-11-24 00:29:24 +13:00
|
|
|
screen,
|
|
|
|
selectedComponentId,
|
2021-01-06 23:11:56 +13:00
|
|
|
previewType: $store.currentFrontEndType,
|
2021-06-28 23:55:11 +12:00
|
|
|
theme: $store.theme,
|
2021-09-02 22:38:41 +12:00
|
|
|
customTheme: $store.customTheme,
|
2021-09-08 03:02:11 +12:00
|
|
|
previewDevice: $store.previewDevice,
|
2020-02-12 05:36:16 +13:00
|
|
|
}
|
|
|
|
|
2020-12-01 01:11:50 +13:00
|
|
|
// Saving pages and screens to the DB causes them to have _revs.
|
|
|
|
// These revisions change every time a save happens and causes
|
|
|
|
// these reactive statements to fire, even though the actual
|
|
|
|
// definition hasn't changed.
|
|
|
|
// By deleting all _rev properties we can avoid this and increase
|
|
|
|
// performance.
|
2020-12-02 05:22:06 +13:00
|
|
|
$: json = JSON.stringify(previewData)
|
2021-08-24 18:51:38 +12:00
|
|
|
$: strippedJson = json.replace(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
|
2020-02-12 05:36:16 +13:00
|
|
|
|
2020-11-24 00:29:24 +13:00
|
|
|
// Update the iframe with the builder info to render the correct preview
|
2020-12-01 01:11:50 +13:00
|
|
|
const refreshContent = message => {
|
2020-11-24 00:29:24 +13:00
|
|
|
if (iframe) {
|
2020-12-01 01:11:50 +13:00
|
|
|
iframe.contentWindow.postMessage(message)
|
2020-11-24 00:29:24 +13:00
|
|
|
}
|
2020-06-09 08:13:19 +12:00
|
|
|
}
|
2020-06-09 17:22:00 +12:00
|
|
|
|
2020-11-24 03:27:45 +13:00
|
|
|
// Refresh the preview when required
|
2020-12-01 01:11:50 +13:00
|
|
|
$: refreshContent(strippedJson)
|
2020-11-24 00:29:24 +13:00
|
|
|
|
|
|
|
onMount(() => {
|
2021-01-06 23:11:56 +13:00
|
|
|
// Initialise the app when mounted
|
2020-12-01 01:11:50 +13:00
|
|
|
iframe.contentWindow.addEventListener(
|
2021-07-01 06:37:03 +12:00
|
|
|
"ready",
|
2021-07-08 00:54:44 +12:00
|
|
|
() => {
|
|
|
|
// Display preview immediately if the intelligent loading feature
|
|
|
|
// is not supported
|
|
|
|
if (!$store.clientFeatures.intelligentLoading) {
|
|
|
|
loading = false
|
|
|
|
}
|
|
|
|
refreshContent(strippedJson)
|
|
|
|
},
|
2021-01-06 23:11:56 +13:00
|
|
|
{ once: true }
|
2020-12-01 01:11:50 +13:00
|
|
|
)
|
2021-01-06 23:11:56 +13:00
|
|
|
|
2021-07-01 06:37:03 +12:00
|
|
|
// Catch any app errors
|
|
|
|
iframe.contentWindow.addEventListener(
|
|
|
|
"error",
|
|
|
|
event => {
|
|
|
|
loading = false
|
|
|
|
error = event.detail || "An unknown error occurred"
|
|
|
|
},
|
|
|
|
{ once: true }
|
|
|
|
)
|
|
|
|
|
2021-09-01 01:46:30 +12:00
|
|
|
// Add listener for events sent by client library in preview
|
|
|
|
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
|
|
|
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
2020-11-24 00:29:24 +13:00
|
|
|
})
|
2021-09-16 18:35:19 +12:00
|
|
|
|
2021-09-17 04:34:40 +12:00
|
|
|
// Remove all iframe event listeners on component destroy
|
2021-07-27 09:53:11 +12:00
|
|
|
onDestroy(() => {
|
2021-09-17 03:27:19 +12:00
|
|
|
if (iframe.contentWindow) {
|
|
|
|
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
|
|
|
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
|
|
|
|
}
|
2020-11-24 00:29:24 +13:00
|
|
|
})
|
2021-06-24 01:21:37 +12:00
|
|
|
|
2021-09-01 01:46:30 +12:00
|
|
|
const handleBudibaseEvent = event => {
|
|
|
|
const { type, data } = event.detail
|
|
|
|
if (type === "select-component" && data.id) {
|
|
|
|
store.actions.components.select({ _id: data.id })
|
|
|
|
} else if (type === "update-prop") {
|
|
|
|
store.actions.components.updateProp(data.prop, data.value)
|
|
|
|
} else if (type === "delete-component" && data.id) {
|
|
|
|
confirmDeleteComponent(data.id)
|
|
|
|
} else if (type === "preview-loaded") {
|
|
|
|
// Wait for this event to show the client library if intelligent
|
|
|
|
// loading is supported
|
|
|
|
loading = false
|
2021-09-17 04:34:40 +12:00
|
|
|
} else if (type === "move-component") {
|
2021-09-17 04:39:39 +12:00
|
|
|
const { componentId, destinationComponentId } = data
|
|
|
|
const rootComponent = get(currentAsset).props
|
|
|
|
|
|
|
|
// Get source and destination components
|
|
|
|
const source = findComponent(rootComponent, componentId)
|
|
|
|
const destination = findComponent(rootComponent, destinationComponentId)
|
2021-09-17 04:34:40 +12:00
|
|
|
|
|
|
|
// Stop if the target is a child of source
|
2021-09-17 04:39:39 +12:00
|
|
|
const path = findComponentPath(source, destinationComponentId)
|
2021-09-17 04:34:40 +12:00
|
|
|
const ids = path.map(component => component._id)
|
|
|
|
if (ids.includes(data.destinationComponentId)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-17 04:39:39 +12:00
|
|
|
// Cut and paste the component to the new destination
|
|
|
|
if (source && destination) {
|
|
|
|
store.actions.components.copy(source, true)
|
|
|
|
store.actions.components.paste(destination, data.mode)
|
2021-09-17 04:34:40 +12:00
|
|
|
}
|
2021-09-01 01:46:30 +12:00
|
|
|
} else {
|
|
|
|
console.warning(`Client sent unknown event type: ${type}`)
|
|
|
|
}
|
2021-09-17 03:27:19 +12:00
|
|
|
}
|
2021-09-01 01:46:30 +12:00
|
|
|
|
|
|
|
const handleKeydownEvent = event => {
|
2021-09-17 03:27:19 +12:00
|
|
|
if (
|
|
|
|
(event.key === "Delete" || event.key === "Backspace") &&
|
2021-09-01 01:46:30 +12:00
|
|
|
selectedComponentId &&
|
2021-09-17 03:27:19 +12:00
|
|
|
["input", "textarea"].indexOf(
|
|
|
|
iframe.contentWindow.document.activeElement?.tagName.toLowerCase()
|
|
|
|
) === -1
|
|
|
|
) {
|
|
|
|
confirmDeleteComponent(selectedComponentId)
|
2021-09-01 01:46:30 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-17 03:27:19 +12:00
|
|
|
const confirmDeleteComponent = componentId => {
|
2021-07-27 09:48:59 +12:00
|
|
|
idToDelete = componentId
|
|
|
|
confirmDeleteDialog.show()
|
|
|
|
}
|
|
|
|
|
2021-10-05 04:50:52 +13:00
|
|
|
const deleteComponent = async () => {
|
|
|
|
try {
|
|
|
|
await store.actions.components.delete({ _id: idToDelete })
|
|
|
|
} catch (error) {
|
|
|
|
notifications.error(error)
|
|
|
|
}
|
2021-06-24 01:21:37 +12:00
|
|
|
idToDelete = null
|
|
|
|
}
|
2021-10-05 04:50:52 +13:00
|
|
|
|
2021-06-24 01:21:37 +12:00
|
|
|
const cancelDeleteComponent = () => {
|
|
|
|
idToDelete = null
|
|
|
|
}
|
2020-02-12 05:36:16 +13:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class="component-container">
|
2021-07-01 06:37:03 +12:00
|
|
|
{#if loading}
|
|
|
|
<div class="center">
|
|
|
|
<ProgressCircle />
|
|
|
|
</div>
|
|
|
|
{:else if error}
|
|
|
|
<div class="center error">
|
|
|
|
<Layout justifyItems="center" gap="S">
|
|
|
|
{@html ErrorSVG}
|
|
|
|
<Heading size="L">App preview failed to load</Heading>
|
|
|
|
<Body size="S">{error}</Body>
|
|
|
|
</Layout>
|
|
|
|
</div>
|
|
|
|
{/if}
|
2020-12-08 23:16:01 +13:00
|
|
|
<iframe
|
|
|
|
title="componentPreview"
|
|
|
|
bind:this={iframe}
|
2021-05-07 03:39:34 +12:00
|
|
|
srcdoc={template}
|
2021-07-01 06:37:03 +12:00
|
|
|
class:hidden={loading || error}
|
2021-09-08 03:02:11 +12:00
|
|
|
class:tablet={$store.previewDevice === "tablet"}
|
|
|
|
class:mobile={$store.previewDevice === "mobile"}
|
2021-05-07 03:39:34 +12:00
|
|
|
/>
|
2019-08-20 08:18:23 +12:00
|
|
|
</div>
|
2021-06-24 01:21:37 +12:00
|
|
|
<ConfirmDialog
|
|
|
|
bind:this={confirmDeleteDialog}
|
|
|
|
title="Confirm Deletion"
|
|
|
|
body={`Are you sure you want to delete this component?`}
|
|
|
|
okText="Delete component"
|
|
|
|
onOk={deleteComponent}
|
|
|
|
onCancel={cancelDeleteComponent}
|
|
|
|
/>
|
2019-08-20 08:18:23 +12:00
|
|
|
|
|
|
|
<style>
|
2020-02-12 05:36:16 +13:00
|
|
|
.component-container {
|
|
|
|
grid-row-start: middle;
|
|
|
|
grid-column-start: middle;
|
2021-09-08 03:02:11 +12:00
|
|
|
display: grid;
|
|
|
|
place-items: center;
|
2020-02-12 05:36:16 +13:00
|
|
|
position: relative;
|
|
|
|
overflow: hidden;
|
|
|
|
margin: auto;
|
2020-04-24 21:35:54 +12:00
|
|
|
height: 100%;
|
2020-02-12 05:36:16 +13:00
|
|
|
}
|
|
|
|
.component-container iframe {
|
|
|
|
border: 0;
|
|
|
|
left: 0;
|
|
|
|
top: 0;
|
|
|
|
width: 100%;
|
2021-06-09 01:19:03 +12:00
|
|
|
background-color: transparent;
|
2020-02-12 05:36:16 +13:00
|
|
|
}
|
2021-07-01 06:37:03 +12:00
|
|
|
.center {
|
|
|
|
position: absolute;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
display: grid;
|
|
|
|
place-items: center;
|
|
|
|
z-index: 1;
|
|
|
|
}
|
|
|
|
.hidden {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
.error :global(svg) {
|
|
|
|
fill: var(--spectrum-global-color-gray-500);
|
|
|
|
width: 80px;
|
|
|
|
height: 80px;
|
|
|
|
}
|
|
|
|
.error :global(h1),
|
|
|
|
.error :global(p) {
|
|
|
|
color: var(--spectrum-global-color-gray-800);
|
|
|
|
}
|
|
|
|
.error :global(p) {
|
|
|
|
font-style: italic;
|
|
|
|
margin-top: -0.5em;
|
|
|
|
}
|
|
|
|
.error :global(h1) {
|
|
|
|
font-weight: 400;
|
|
|
|
margin: 0;
|
|
|
|
}
|
2021-09-08 03:02:11 +12:00
|
|
|
|
|
|
|
iframe {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
}
|
2020-02-19 04:53:22 +13:00
|
|
|
</style>
|