Enable selecting components from the builder preview and apply any custom styles

This commit is contained in:
Andrew Kingston 2021-01-06 10:13:30 +00:00
parent c101715c0e
commit a40bf95c41
4 changed files with 93 additions and 14 deletions

View File

@ -9,6 +9,7 @@
setContext("sdk", SDK) setContext("sdk", SDK)
setContext("component", writable({})) setContext("component", writable({}))
setContext("data", createDataStore()) setContext("data", createDataStore())
setContext("screenslot", false)
let loaded = false let loaded = false

View File

@ -8,8 +8,9 @@
export let definition = {} export let definition = {}
// Get local data binding context // Get contexts
const dataContext = getContext("data") const dataContext = getContext("data")
const screenslotContext = getContext("screenslot")
// Create component context // Create component context
const componentStore = writable({}) const componentStore = writable({})
@ -20,10 +21,15 @@
$: children = definition._children $: children = definition._children
$: id = definition._id $: id = definition._id
$: enrichedProps = enrichProps(definition, $dataContext, $bindingStore) $: enrichedProps = enrichProps(definition, $dataContext, $bindingStore)
$: selected = id === $builderStore.selectedComponentId $: styles = definition._styles
// Allow component selection in the builder preview if we're previewing a
// layout, or we're preview a screen and we're inside the screenslot
$: allowSelection =
$builderStore.previewType === "layout" || screenslotContext
// Update component context // Update component context
$: componentStore.set({ id, styles: { ...definition._styles, selected } }) $: componentStore.set({ id, styles: { ...styles, id, allowSelection } })
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
const getComponentConstructor = component => { const getComponentConstructor = component => {

View File

@ -1,5 +1,5 @@
<script> <script>
import { getContext } from "svelte" import { getContext, setContext } from "svelte"
import Router from "svelte-spa-router" import Router from "svelte-spa-router"
import { routeStore } from "../store" import { routeStore } from "../store"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
@ -7,6 +7,9 @@
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// Set context flag so components know that we're now inside the screenslot
setContext("screenslot", true)
// Only wrap this as an array to take advantage of svelte keying, // Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config // to ensure the svelte-spa-router is fully remounted when route config
// changes // changes

View File

@ -1,49 +1,111 @@
import { get } from "svelte/store"
import { builderStore } from "../store"
const selectedComponentWidth = 2
const selectedComponentColor = "#4285f4"
/** /**
* Helper to build a CSS string from a style object * Helper to build a CSS string from a style object.
*/ */
const buildStyleString = (styleObject, customStyles, selected) => { const buildStyleString = (styleObject, customStyles) => {
let str = "" let str = ""
Object.entries(styleObject).forEach(([style, value]) => { Object.entries(styleObject).forEach(([style, value]) => {
if (style && value != null) { if (style && value != null) {
str += `${style}: ${value}; ` str += `${style}: ${value}; `
} }
}) })
str += customStyles || "" return str + (customStyles || "")
if (selected) { }
str += ";border: 2px solid #0055ff !important;"
/**
* Applies styles to enrich the builder preview.
* Applies styles to highlight the selected component, and allows pointer
* events for any selectable components (overriding the blanket ban on pointer
* events in the iframe HTML).
*/
const addBuilderPreviewStyles = (styleString, componentId, selectable) => {
let str = styleString
// Apply extra styles if we're in the builder preview
const state = get(builderStore)
if (state.inBuilder) {
// Allow pointer events and always enable cursor
if (selectable) {
str += ";pointer-events: all !important; cursor: pointer !important;"
}
// Highlighted selected element
if (componentId === state.selectedComponentId) {
str += `;box-shadow: 0 0 0 ${selectedComponentWidth}px ${selectedComponentColor} inset !important;`
}
} }
return str return str
} }
/** /**
* Svelte action to apply correct component styles. * Svelte action to apply correct component styles.
* This also applies handlers for selecting components from the builder preview.
*/ */
export const styleable = (node, styles = {}) => { export const styleable = (node, styles = {}) => {
let applyNormalStyles let applyNormalStyles
let applyHoverStyles let applyHoverStyles
let selectComponent
// Kill JS even bubbling
const blockEvent = event => {
event.preventDefault()
event.stopPropagation()
return false
}
// Creates event listeners and applies initial styles // Creates event listeners and applies initial styles
const setupStyles = newStyles => { const setupStyles = newStyles => {
const selected = newStyles.selected const componentId = newStyles.id
const selectable = newStyles.allowSelection
const customStyles = newStyles.custom const customStyles = newStyles.custom
const normalStyles = newStyles.normal || {} const normalStyles = newStyles.normal
const hoverStyles = { const hoverStyles = {
...normalStyles, ...normalStyles,
...newStyles.hover, ...newStyles.hover,
} }
applyNormalStyles = () => { // Applies a style string to a DOM node, enriching it for the builder
node.style = buildStyleString(normalStyles, customStyles, selected) // preview
const applyStyles = styleString => {
node.style = addBuilderPreviewStyles(styleString, componentId, selectable)
} }
// Applies the "normal" style definition
applyNormalStyles = () => {
applyStyles(buildStyleString(normalStyles, customStyles))
}
// Applies any "hover" styles as well as the base "normal" styles
applyHoverStyles = () => { applyHoverStyles = () => {
node.style = buildStyleString(hoverStyles, customStyles, selected) applyStyles(buildStyleString(hoverStyles, customStyles))
}
// Handler to select a component in the builder when clicking it in the
// builder preview
selectComponent = event => {
builderStore.actions.selectComponent(newStyles.id)
return blockEvent(event)
} }
// Add listeners to toggle hover styles // Add listeners to toggle hover styles
node.addEventListener("mouseover", applyHoverStyles) node.addEventListener("mouseover", applyHoverStyles)
node.addEventListener("mouseout", applyNormalStyles) node.addEventListener("mouseout", applyNormalStyles)
// Add builder preview click listener
if (get(builderStore).inBuilder) {
node.addEventListener("click", selectComponent, false)
// Kill other interaction events
node.addEventListener("mousedown", blockEvent)
node.addEventListener("mouseup", blockEvent)
}
// Apply initial normal styles // Apply initial normal styles
applyNormalStyles() applyNormalStyles()
} }
@ -52,6 +114,13 @@ export const styleable = (node, styles = {}) => {
const removeListeners = () => { const removeListeners = () => {
node.removeEventListener("mouseover", applyHoverStyles) node.removeEventListener("mouseover", applyHoverStyles)
node.removeEventListener("mouseout", applyNormalStyles) node.removeEventListener("mouseout", applyNormalStyles)
// Remove builder preview click listener
if (get(builderStore).inBuilder) {
node.removeEventListener("click", selectComponent)
node.removeEventListener("mousedown", blockEvent)
node.removeEventListener("mouseup", blockEvent)
}
} }
// Apply initial styles // Apply initial styles