Merge pull request #4925 from Budibase/feature/map-actions

Button action inputs + map component actions
This commit is contained in:
Andrew Kingston 2022-03-25 12:28:56 +00:00 committed by GitHub
commit fa7136d5ea
12 changed files with 427 additions and 213 deletions

View File

@ -5,7 +5,7 @@ version: "3"
services: services:
minio-service: minio-service:
container_name: budi-minio-dev container_name: budi-minio-dev
restart: always restart: on-failure
image: minio/minio image: minio/minio
volumes: volumes:
- minio_data:/data - minio_data:/data
@ -23,7 +23,7 @@ services:
proxy-service: proxy-service:
container_name: budi-nginx-dev container_name: budi-nginx-dev
restart: always restart: on-failure
image: nginx:latest image: nginx:latest
volumes: volumes:
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf - ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
@ -38,7 +38,7 @@ services:
couchdb-service: couchdb-service:
# platform: linux/amd64 # platform: linux/amd64
container_name: budi-couchdb-dev container_name: budi-couchdb-dev
restart: always restart: on-failure
image: ibmcom/couchdb3 image: ibmcom/couchdb3
environment: environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
@ -59,7 +59,7 @@ services:
redis-service: redis-service:
container_name: budi-redis-dev container_name: budi-redis-dev
restart: always restart: on-failure
image: redis image: redis
command: redis-server --requirepass ${REDIS_PASSWORD} command: redis-server --requirepass ${REDIS_PASSWORD}
ports: ports:

View File

@ -393,18 +393,45 @@ const getUrlBindings = asset => {
/** /**
* Gets all bindable properties exposed in a button actions flow up until * Gets all bindable properties exposed in a button actions flow up until
* the specified action ID. * the specified action ID, as well as context provided for the action
* setting as a whole by the component.
*/ */
export const getButtonContextBindings = (actions, actionId) => { export const getButtonContextBindings = (
asset,
componentId,
settingKey,
actions,
actionId
) => {
let bindings = []
// Check if any context bindings are provided by the component for this
// setting
const component = findComponent(asset.props, componentId)
const settings = getComponentSettings(component?._component)
const eventSetting = settings.find(setting => setting.key === settingKey)
if (!eventSetting) {
return bindings
}
if (eventSetting.context?.length) {
eventSetting.context.forEach(contextEntry => {
bindings.push({
readableBinding: contextEntry.label,
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
contextEntry.key
)}`,
})
})
}
// Get the steps leading up to this value // Get the steps leading up to this value
const index = actions?.findIndex(action => action.id === actionId) const index = actions?.findIndex(action => action.id === actionId)
if (index == null || index === -1) { if (index == null || index === -1) {
return [] return bindings
} }
const prevActions = actions.slice(0, index) const prevActions = actions.slice(0, index)
// Generate bindings for any steps which provide context // Generate bindings for any steps which provide context
let bindings = []
prevActions.forEach((action, idx) => { prevActions.forEach((action, idx) => {
const def = ActionDefinitions.actions.find( const def = ActionDefinitions.actions.find(
x => x.name === action["##eventHandlerType"] x => x.name === action["##eventHandlerType"]
@ -418,6 +445,7 @@ export const getButtonContextBindings = (actions, actionId) => {
}) })
} }
}) })
return bindings return bindings
} }

View File

@ -2,9 +2,10 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => { export const getThemeStore = () => {
const themeElement = document.documentElement const themeElement = document.documentElement
const initialValue = { const initialValue = {
theme: "darkest", theme: "darkest",
options: ["lightest", "light", "dark", "darkest"], options: ["lightest", "light", "dark", "darkest", "nord"],
} }
const store = createLocalStorageStore("bb-theme", initialValue) const store = createLocalStorageStore("bb-theme", initialValue)
@ -21,6 +22,7 @@ export const getThemeStore = () => {
`spectrum--${option}`, `spectrum--${option}`,
option === state.theme option === state.theme
) )
themeElement.classList.add("spectrum--darkest")
}) })
}) })

View File

@ -12,11 +12,13 @@
import { getAvailableActions } from "./index" import { getAvailableActions } from "./index"
import { generate } from "shortid" import { generate } from "shortid"
import { getButtonContextBindings } from "builderStore/dataBinding" import { getButtonContextBindings } from "builderStore/dataBinding"
import { currentAsset, store } from "builderStore"
const flipDurationMs = 150 const flipDurationMs = 150
const EVENT_TYPE_KEY = "##eventHandlerType" const EVENT_TYPE_KEY = "##eventHandlerType"
const actionTypes = getAvailableActions() const actionTypes = getAvailableActions()
export let key
export let actions export let actions
export let bindings = [] export let bindings = []
@ -24,6 +26,9 @@
// These are ephemeral bindings which only exist while executing actions // These are ephemeral bindings which only exist while executing actions
$: buttonContextBindings = getButtonContextBindings( $: buttonContextBindings = getButtonContextBindings(
$currentAsset,
$store.selectedComponentId,
key,
actions, actions,
selectedAction?.id selectedAction?.id
) )

View File

@ -8,6 +8,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let key
export let value = [] export let value = []
export let name export let name
export let bindings export let bindings
@ -81,5 +82,6 @@
bind:actions={tmpValue} bind:actions={tmpValue}
eventType={name} eventType={name}
{bindings} {bindings}
{key}
/> />
</Drawer> </Drawer>

View File

@ -79,6 +79,7 @@
bindings={allBindings} bindings={allBindings}
name={key} name={key}
text={label} text={label}
{key}
{type} {type}
{...props} {...props}
/> />

View File

@ -2530,67 +2530,102 @@
"styles": ["size"], "styles": ["size"],
"editable": true, "editable": true,
"draggable": false, "draggable": false,
"illegalChildren": [ "illegalChildren": ["section"],
"section"
],
"settings": [ "settings": [
{ {
"type": "dataProvider", "type": "dataProvider",
"label": "Provider", "label": "Provider",
"key": "dataProvider" "key": "dataProvider"
}, },
{
"type": "field",
"label": "Latitude Key",
"key": "latitudeKey",
"dependsOn": "dataProvider"
},
{
"type": "field",
"label": "Longitude Key",
"key": "longitudeKey",
"dependsOn": "dataProvider"
},
{
"type": "field",
"label": "Title Key",
"key": "titleKey",
"dependsOn": "dataProvider"
},
{
"type": "event",
"label": "On Click Marker",
"key": "onClickMarker",
"context": [
{
"label": "Clicked marker",
"key": "marker"
}
]
},
{ {
"type": "boolean", "type": "boolean",
"label": "Enable Fullscreen", "label": "Enable creating markers",
"key": "creationEnabled",
"defaultValue": false
},
{
"type": "event",
"label": "On Create Marker",
"key": "onCreateMarker",
"dependsOn": "creationEnabled",
"context": [
{
"label": "New marker latitude",
"key": "lat"
},
{
"label": "New marker longitude",
"key": "lng"
}
]
},
{
"type": "boolean",
"label": "Enable fullscreen",
"key": "fullScreenEnabled", "key": "fullScreenEnabled",
"defaultValue": true "defaultValue": true
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Enable Location", "label": "Enable location",
"key": "locationEnabled", "key": "locationEnabled",
"defaultValue": true "defaultValue": true
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Enable Zoom", "label": "Enable zoom",
"key": "zoomEnabled", "key": "zoomEnabled",
"defaultValue": true "defaultValue": true
}, },
{
"type": "number",
"label": "Zoom Level (0-100)",
"key": "zoomLevel",
"defaultValue": 72,
"max" : 100,
"min" : 0
},
{
"type": "field",
"label": "Latitude Key",
"key": "latitudeKey"
},
{
"type": "field",
"label": "Longitude Key",
"key": "longitudeKey"
},
{
"type": "field",
"label": "Title Key",
"key": "titleKey"
},
{ {
"type": "text", "type": "text",
"label": "Tile URL", "label": "Tile URL",
"key": "tileURL", "key": "tileURL",
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" "defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
}, },
{ {
"type": "text", "type": "text",
"label": "Default Location (lat,lng)", "label": "Default Location (when empty)",
"key": "defaultLocation", "key": "defaultLocation",
"defaultValue": "51.5072,-0.1276" "placeholder": "51.5072,-0.1276"
},
{
"type": "number",
"label": "Default Location Zoom Level (0-100)",
"key": "zoomLevel",
"placeholder": 50,
"max": 100,
"min": 0
}, },
{ {
"type": "text", "type": "text",

View File

@ -1,6 +1,7 @@
<script context="module"> <script context="module">
// Cache the definition of settings for each component type // Cache the definition of settings for each component type
let SettingsDefinitionCache = {} let SettingsDefinitionCache = {}
let SettingsDefinitionMapCache = {}
// Cache the settings of each component ID. // Cache the settings of each component ID.
// This speeds up remounting as well as repeaters. // This speeds up remounting as well as repeaters.
@ -74,6 +75,8 @@
// Component information derived during initialisation // Component information derived during initialisation
let constructor let constructor
let definition let definition
let settingsDefinition
let settingsDefinitionMap
// Set up initial state for each new component instance // Set up initial state for each new component instance
$: initialise(instance) $: initialise(instance)
@ -118,7 +121,7 @@
$: emptyState = empty && showEmptyState $: emptyState = empty && showEmptyState
// Enrich component settings // Enrich component settings
$: enrichComponentSettings($context) $: enrichComponentSettings($context, settingsDefinitionMap)
// Evaluate conditional UI settings and store any component setting changes // Evaluate conditional UI settings and store any component setting changes
// which need to be made. This is broken into 2 lines to avoid svelte // which need to be made. This is broken into 2 lines to avoid svelte
@ -168,12 +171,14 @@
} }
// Get the settings definition for this component, and cache it // Get the settings definition for this component, and cache it
let settingsDefinition
if (SettingsDefinitionCache[definition.name]) { if (SettingsDefinitionCache[definition.name]) {
settingsDefinition = SettingsDefinitionCache[definition.name] settingsDefinition = SettingsDefinitionCache[definition.name]
settingsDefinitionMap = SettingsDefinitionMapCache[definition.name]
} else { } else {
settingsDefinition = getSettingsDefinition(definition) settingsDefinition = getSettingsDefinition(definition)
settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition)
SettingsDefinitionCache[definition.name] = settingsDefinition SettingsDefinitionCache[definition.name] = settingsDefinition
SettingsDefinitionMapCache[definition.name] = settingsDefinitionMap
} }
// Parse the instance settings, and cache them // Parse the instance settings, and cache them
@ -190,7 +195,9 @@
dynamicSettings = instanceSettings.dynamicSettings dynamicSettings = instanceSettings.dynamicSettings
// Force an initial enrichment of the new settings // Force an initial enrichment of the new settings
enrichComponentSettings(get(context), { force: true }) enrichComponentSettings(get(context), settingsDefinitionMap, {
force: true,
})
} }
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
@ -226,6 +233,14 @@
return settings return settings
} }
const getSettingsDefinitionMap = settingsDefinition => {
let map = {}
settingsDefinition?.forEach(setting => {
map[setting.key] = setting
})
return map
}
const getInstanceSettings = (instance, settingsDefinition) => { const getInstanceSettings = (instance, settingsDefinition) => {
// Get raw settings // Get raw settings
let settings = {} let settings = {}
@ -248,7 +263,7 @@
} else if (typeof value === "string" && value.includes("{{")) { } else if (typeof value === "string" && value.includes("{{")) {
// Strings can be trivially checked // Strings can be trivially checked
delete newStaticSettings[setting.key] delete newStaticSettings[setting.key]
} else if (value[0]?.["##eventHandlerType"] != null) { } else if (setting.type === "event") {
// Always treat button actions as dynamic // Always treat button actions as dynamic
delete newStaticSettings[setting.key] delete newStaticSettings[setting.key]
} else if (typeof value === "object") { } else if (typeof value === "object") {
@ -273,7 +288,11 @@
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const enrichComponentSettings = (context, options = { force: false }) => { const enrichComponentSettings = (
context,
settingsDefinitionMap,
options = { force: false }
) => {
const contextChanged = context.key !== lastContextKey const contextChanged = context.key !== lastContextKey
if (!contextChanged && !options?.force) { if (!contextChanged && !options?.force) {
return return
@ -285,7 +304,11 @@
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
// Enrich settings with context // Enrich settings with context
const newEnrichedSettings = enrichProps(dynamicSettings, context) const newEnrichedSettings = enrichProps(
dynamicSettings,
context,
settingsDefinitionMap
)
// Abandon this update if a newer update has started // Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) { if (enrichmentTime !== latestUpdateTime) {

View File

@ -2,8 +2,8 @@
import L from "leaflet" import L from "leaflet"
import sanitizeHtml from "sanitize-html" import sanitizeHtml from "sanitize-html"
import "leaflet/dist/leaflet.css" import "leaflet/dist/leaflet.css"
import { Helpers } from "@budibase/bbui" import { Helpers, Button } from "@budibase/bbui"
import { getContext } from "svelte" import { onMount, getContext } from "svelte"
import { import {
FullScreenControl, FullScreenControl,
LocationControl, LocationControl,
@ -24,86 +24,16 @@
export let defaultLocation export let defaultLocation
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
export let mapAttribution export let mapAttribution
export let creationEnabled = false
export let onClickMarker
export let onCreateMarker
const { styleable, notificationStore } = getContext("sdk") const { styleable, notificationStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
const embeddedMapId = `${Helpers.uuid()}-wrapper` const embeddedMapId = `${Helpers.uuid()}-wrapper`
let cachedDeviceCoordinates
const fallbackCoordinates = [51.5072, -0.1276] //London
let mapInstance
let mapMarkerGroup = new L.FeatureGroup()
let mapMarkers = []
let minZoomLevel = 0
let maxZoomLevel = 18
let adjustedZoomLevel = !Number.isInteger(zoomLevel)
? 72
: Math.round(zoomLevel * (maxZoomLevel / 100))
$: zoomControlUpdated(mapInstance, zoomEnabled)
$: locationControlUpdated(mapInstance, locationEnabled)
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
$: updateMapDimensions(
mapInstance,
$component.styles.normal.width,
$component.styles.normal.height
)
$: addMapMarkers(
mapInstance,
dataProvider?.rows,
latitudeKey,
longitudeKey,
titleKey
)
$: if (typeof mapInstance === "object" && mapMarkers.length > 0) {
mapInstance.setZoom(0)
mapInstance.fitBounds(mapMarkerGroup.getBounds(), {
paddingTopLeft: [0, 24],
})
}
const updateMapDimensions = mapInstance => {
if (typeof mapInstance !== "object") {
return
}
mapInstance.invalidateSize()
}
let isValidLatitude = value => {
return !isNaN(value) && value > -90 && value < 90
}
let isValidLongitude = value => {
return !isNaN(value) && value > -180 && value < 180
}
const parseDefaultLocation = defaultLocation => {
if (typeof defaultLocation !== "string") {
return fallbackCoordinates
}
let defaultLocationParts = defaultLocation.split(",")
if (defaultLocationParts.length !== 2) {
return fallbackCoordinates
}
let parsedDefaultLatitude = parseFloat(defaultLocationParts[0].trim())
let parsedDefaultLongitude = parseFloat(defaultLocationParts[1].trim())
return isValidLatitude(parsedDefaultLatitude) === true &&
isValidLongitude(parsedDefaultLongitude) === true
? [parsedDefaultLatitude, parsedDefaultLongitude]
: fallbackCoordinates
}
$: defaultCoordinates =
mapMarkers.length > 0
? parseDefaultLocation(defaultLocation)
: fallbackCoordinates
// Map Button Controls // Map Button Controls
let locationControl = new LocationControl({ const locationControl = new LocationControl({
position: "bottomright", position: "bottomright",
onLocationFail: err => { onLocationFail: err => {
if (err.code === GeolocationPositionError.PERMISSION_DENIED) { if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
@ -129,13 +59,135 @@
} }
}, },
}) })
let fullScreenControl = new FullScreenControl({ const fullScreenControl = new FullScreenControl({
position: "topright", position: "topright",
}) })
let zoomControl = L.control.zoom({ const zoomControl = L.control.zoom({
position: "bottomright", position: "bottomright",
}) })
// Map and marker configuration
const defaultMarkerOptions = {
html:
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' +
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>',
className: "embedded-map-marker",
iconSize: [26, 26],
iconAnchor: [13, 26],
popupAnchor: [0, -13],
}
const mapMarkerOptions = {
icon: L.divIcon(defaultMarkerOptions),
draggable: false,
alt: "Location Marker",
}
const candidateMarkerOptions = {
icon: L.divIcon({
...defaultMarkerOptions,
className: "embedded-map-marker--candidate",
}),
draggable: false,
alt: "Location Marker",
}
const mapOptions = {
fullScreen: false,
zoomControl: false,
scrollWheelZoom: zoomEnabled,
minZoomLevel,
maxZoomLevel,
}
const fallbackCoordinates = [51.5072, -0.1276] //London
let mapInstance
let mapMarkerGroup = new L.FeatureGroup()
let candidateMarkerGroup = new L.FeatureGroup()
let candidateMarkerPosition
let mounted = false
let initialMarkerZoomCompleted = false
let minZoomLevel = 0
let maxZoomLevel = 18
let cachedDeviceCoordinates
$: validRows = getValidRows(dataProvider?.rows, latitudeKey, longitudeKey)
$: safeZoomLevel = parseZoomLevel(zoomLevel)
$: defaultCoordinates = parseDefaultLocation(defaultLocation)
$: initMap(tileURL, mapAttribution, safeZoomLevel)
$: zoomControlUpdated(mapInstance, zoomEnabled)
$: locationControlUpdated(mapInstance, locationEnabled)
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
$: width = $component.styles.normal.width
$: height = $component.styles.normal.height
$: width, height, mapInstance?.invalidateSize()
$: defaultCoordinates, resetView()
$: addMapMarkers(
mapInstance,
validRows,
latitudeKey,
longitudeKey,
titleKey,
onClickMarker
)
const isValidLatitude = value => {
return !isNaN(value) && value > -90 && value < 90
}
const isValidLongitude = value => {
return !isNaN(value) && value > -180 && value < 180
}
const getValidRows = (rows, latKey, lngKey) => {
if (!rows?.length || !latKey || !lngKey) {
return []
}
return rows.filter(row => {
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
})
}
const parseZoomLevel = zoomLevel => {
let zoom = zoomLevel
if (zoom == null || isNaN(zoom)) {
zoom = 50
} else {
zoom = parseFloat(zoom)
zoom = Math.max(0, Math.min(100, zoom))
}
return Math.round((zoom * maxZoomLevel) / 100)
}
const parseDefaultLocation = defaultLocation => {
if (typeof defaultLocation !== "string") {
return fallbackCoordinates
}
let defaultLocationParts = defaultLocation.split(",")
if (defaultLocationParts.length !== 2) {
return fallbackCoordinates
}
let parsedDefaultLatitude = parseFloat(defaultLocationParts[0].trim())
let parsedDefaultLongitude = parseFloat(defaultLocationParts[1].trim())
return isValidLatitude(parsedDefaultLatitude) === true &&
isValidLongitude(parsedDefaultLongitude) === true
? [parsedDefaultLatitude, parsedDefaultLongitude]
: fallbackCoordinates
}
const resetView = () => {
if (!mapInstance) {
return
}
if (mapMarkerGroup.getLayers().length) {
mapInstance.setZoom(0)
mapInstance.fitBounds(mapMarkerGroup.getBounds(), {
paddingTopLeft: [0, 24],
})
} else {
mapInstance.setView(defaultCoordinates, safeZoomLevel)
}
}
const locationControlUpdated = (mapInstance, locationEnabled) => { const locationControlUpdated = (mapInstance, locationEnabled) => {
if (typeof mapInstance !== "object") { if (typeof mapInstance !== "object") {
return return
@ -171,44 +223,25 @@
} }
} }
//Map icon and marker configuration const addMapMarkers = (
const mapIconMarkup = mapInstance,
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' + validRows,
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>' latKey,
const mapIcon = L.divIcon({ lngKey,
html: mapIconMarkup, titleKey,
className: "embedded-map-marker", onClick
iconSize: [26, 26], ) => {
iconAnchor: [13, 26], if (!mapInstance) {
popupAnchor: [0, -13],
})
const mapMarkerOptions = {
icon: mapIcon,
draggable: false,
alt: "Location Marker",
}
let mapOptions = {
fullScreen: false,
zoomControl: false,
scrollWheelZoom: zoomEnabled,
minZoomLevel,
maxZoomLevel,
}
const addMapMarkers = (mapInstance, rows, latKey, lngKey, titleKey) => {
if (typeof mapInstance !== "object" || !rows || !latKey || !lngKey) {
return return
} }
mapMarkerGroup.clearLayers() mapMarkerGroup.clearLayers()
if (!validRows?.length) {
const validRows = rows.filter(row => { return
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey]) }
})
validRows.forEach(row => { validRows.forEach(row => {
let markerCoords = [row[latKey], row[lngKey]] let markerCoords = [row[latKey], row[lngKey]]
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance) let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
let markerContent = generateMarkerPopupContent( let markerContent = generateMarkerPopupContent(
row[latKey], row[latKey],
@ -216,52 +249,105 @@
row[titleKey] row[titleKey]
) )
marker.bindPopup(markerContent).addTo(mapMarkerGroup) marker
.bindTooltip(markerContent, {
direction: "top",
offset: [0, -25],
})
.addTo(mapMarkerGroup)
//https://github.com/Leaflet/Leaflet/issues/7331 if (onClick) {
marker.on("click", function () { marker.on("click", () => {
this.openPopup() onClick({
}) marker: row,
})
mapMarkers = [...mapMarkers, marker] })
}
}) })
// Zoom to markers if this is the first time
if (!initialMarkerZoomCompleted) {
resetView()
initialMarkerZoomCompleted = true
}
} }
const generateMarkerPopupContent = (latitude, longitude, text) => { const generateMarkerPopupContent = (latitude, longitude, text) => {
return text || latitude + "," + longitude return text || latitude + "," + longitude
} }
const initMap = () => { const initMap = (tileURL, attribution, zoom) => {
const initCoords = defaultCoordinates if (!mounted) {
return
}
if (mapInstance) {
mapInstance.remove()
}
mapInstance = L.map(embeddedMapId, mapOptions) mapInstance = L.map(embeddedMapId, mapOptions)
mapMarkerGroup.addTo(mapInstance) mapMarkerGroup.addTo(mapInstance)
candidateMarkerGroup.addTo(mapInstance)
const cleanAttribution = sanitizeHtml(mapAttribution, { // Add attribution
const cleanAttribution = sanitizeHtml(attribution, {
allowedTags: ["a"], allowedTags: ["a"],
allowedAttributes: { allowedAttributes: {
a: ["href", "target"], a: ["href", "target"],
}, },
}) })
L.tileLayer(tileURL, { L.tileLayer(tileURL, {
attribution: "&copy; " + cleanAttribution, attribution: "&copy; " + cleanAttribution,
zoom: adjustedZoomLevel, zoom,
}).addTo(mapInstance) }).addTo(mapInstance)
//Initialise the map // Add click handler
mapInstance.setView(initCoords, adjustedZoomLevel) mapInstance.on("click", handleMapClick)
// Reset view
resetView()
} }
const mapAction = () => { const handleMapClick = e => {
initMap() if (!creationEnabled) {
return { return
destroy() { }
mapInstance.remove() candidateMarkerGroup.clearLayers()
mapInstance = undefined candidateMarkerPosition = [e.latlng.lat, e.latlng.lng]
}, let candidateMarker = L.marker(
candidateMarkerPosition,
candidateMarkerOptions
)
candidateMarker
.bindTooltip("New marker", {
permanent: true,
direction: "top",
offset: [0, -25],
})
.addTo(candidateMarkerGroup)
.on("click", clearCandidateMarker)
}
const createMarker = async () => {
if (!onCreateMarker) {
return
}
const res = await onCreateMarker({
lat: candidateMarkerPosition[0],
lng: candidateMarkerPosition[1],
})
if (res !== false) {
clearCandidateMarker()
} }
} }
const clearCandidateMarker = () => {
candidateMarkerGroup.clearLayers()
candidateMarkerPosition = null
}
onMount(() => {
mounted = true
initMap(tileURL, mapAttribution, safeZoomLevel)
})
</script> </script>
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}> <div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
@ -269,12 +355,20 @@
<div>{error}</div> <div>{error}</div>
{/if} {/if}
<div id={embeddedMapId} class="embedded embedded-map" use:mapAction /> <div id={embeddedMapId} class="embedded embedded-map" />
{#if candidateMarkerPosition}
<div class="button-container">
<Button secondary quiet on:click={clearCandidateMarker}>Cancel</Button>
<Button cta on:click={createMarker}>Create marker</Button>
</div>
{/if}
</div> </div>
<style> <style>
.embedded-map-wrapper { .embedded-map-wrapper {
background-color: #f1f1f1; background-color: #f1f1f1;
height: 320px;
} }
.map-default { .map-default {
min-height: 180px; min-height: 180px;
@ -287,6 +381,9 @@
.embedded-map :global(.embedded-map-marker) { .embedded-map :global(.embedded-map-marker) {
color: #ee3b35; color: #ee3b35;
} }
.embedded-map :global(.embedded-map-marker--candidate) {
color: var(--primaryColor);
}
.embedded-map :global(.embedded-map-control) { .embedded-map :global(.embedded-map-control) {
font-size: 22px; font-size: 22px;
} }
@ -294,4 +391,12 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.button-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
</style> </style>

View File

@ -4,30 +4,36 @@ const initialState = {
showConfirmation: false, showConfirmation: false,
title: null, title: null,
text: null, text: null,
callback: null, onConfirm: null,
onCancel: null,
} }
const createConfirmationStore = () => { const createConfirmationStore = () => {
const store = writable(initialState) const store = writable(initialState)
const showConfirmation = (title, text, callback) => { const showConfirmation = (title, text, onConfirm, onCancel) => {
store.set({ store.set({
showConfirmation: true, showConfirmation: true,
title, title,
text, text,
callback, onConfirm,
onCancel,
}) })
} }
const confirm = async () => { const confirm = async () => {
const state = get(store) const state = get(store)
if (!state.showConfirmation || !state.callback) { if (!state.showConfirmation || !state.onConfirm) {
return return
} }
store.set(initialState) store.set(initialState)
await state.callback() await state.onConfirm()
} }
const cancel = () => { const cancel = () => {
const state = get(store)
store.set(initialState) store.set(initialState)
if (state.onCancel) {
state.onCancel()
}
} }
return { return {

View File

@ -308,7 +308,7 @@ export const enrichButtonActions = (actions, context) => {
let buttonContext = context.actions || [] let buttonContext = context.actions || []
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]]) const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => { return async eventContext => {
for (let i = 0; i < handlers.length; i++) { for (let i = 0; i < handlers.length; i++) {
try { try {
// Skip any non-existent action definitions // Skip any non-existent action definitions
@ -317,7 +317,11 @@ export const enrichButtonActions = (actions, context) => {
} }
// Built total context for this action // Built total context for this action
const totalContext = { ...context, actions: buttonContext } const totalContext = {
...context,
actions: buttonContext,
eventContext,
}
// Get and enrich this button action with the total context // Get and enrich this button action with the total context
let action = actions[i] let action = actions[i]
@ -327,33 +331,36 @@ export const enrichButtonActions = (actions, context) => {
// If this action is confirmable, show confirmation and await a // If this action is confirmable, show confirmation and await a
// callback to execute further actions // callback to execute further actions
if (action.parameters?.confirm) { if (action.parameters?.confirm) {
const defaultText = confirmTextMap[action["##eventHandlerType"]] return new Promise(resolve => {
const confirmText = action.parameters?.confirmText || defaultText const defaultText = confirmTextMap[action["##eventHandlerType"]]
confirmationStore.actions.showConfirmation( const confirmText = action.parameters?.confirmText || defaultText
action["##eventHandlerType"], confirmationStore.actions.showConfirmation(
confirmText, action["##eventHandlerType"],
async () => { confirmText,
// When confirmed, execute this action immediately, async () => {
// then execute the rest of the actions in the chain // When confirmed, execute this action immediately,
const result = await callback() // then execute the rest of the actions in the chain
if (result !== false) { const result = await callback()
// Generate a new total context to pass into the next enrichment if (result !== false) {
buttonContext.push(result) // Generate a new total context to pass into the next enrichment
const newContext = { ...context, actions: buttonContext } buttonContext.push(result)
const newContext = { ...context, actions: buttonContext }
// Enrich and call the next button action // Enrich and call the next button action
const next = enrichButtonActions( const next = enrichButtonActions(
actions.slice(i + 1), actions.slice(i + 1),
newContext newContext
) )
await next() resolve(await next())
} else {
resolve(false)
}
},
() => {
resolve(false)
} }
} )
) })
// Stop enriching actions when encountering a confirmable action,
// as the callback continues the action chain
return
} }
// For non-confirmable actions, execute the handler immediately // For non-confirmable actions, execute the handler immediately

View File

@ -22,7 +22,7 @@ export const propsAreSame = (a, b) => {
* Enriches component props. * Enriches component props.
* Data bindings are enriched, and button actions are enriched. * Data bindings are enriched, and button actions are enriched.
*/ */
export const enrichProps = (props, context) => { export const enrichProps = (props, context, settingsDefinitionMap) => {
// Create context of all bindings and data contexts // Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires // Duplicate the closest context as "data" which the builder requires
const totalContext = { const totalContext = {
@ -38,7 +38,7 @@ export const enrichProps = (props, context) => {
let normalProps = { ...props } let normalProps = { ...props }
let actionProps = {} let actionProps = {}
Object.keys(normalProps).forEach(prop => { Object.keys(normalProps).forEach(prop => {
if (prop?.toLowerCase().includes("onclick")) { if (settingsDefinitionMap?.[prop]?.type === "event") {
actionProps[prop] = normalProps[prop] actionProps[prop] = normalProps[prop]
delete normalProps[prop] delete normalProps[prop]
} }
@ -61,7 +61,7 @@ export const enrichProps = (props, context) => {
// Conditions // Conditions
if (enrichedProps._conditions?.length) { if (enrichedProps._conditions?.length) {
enrichedProps._conditions.forEach((condition, idx) => { enrichedProps._conditions.forEach((condition, idx) => {
if (condition.setting?.toLowerCase().includes("onclick")) { if (settingsDefinitionMap?.[condition.setting]?.type === "event") {
// Use the original condition action value to enrich it to a button // Use the original condition action value to enrich it to a button
// action // action
condition.settingValue = enrichButtonActions( condition.settingValue = enrichButtonActions(