Merge pull request #4925 from Budibase/feature/map-actions
Button action inputs + map component actions
This commit is contained in:
commit
fa7136d5ea
|
@ -5,7 +5,7 @@ version: "3"
|
|||
services:
|
||||
minio-service:
|
||||
container_name: budi-minio-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: minio/minio
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
@ -23,7 +23,7 @@ services:
|
|||
|
||||
proxy-service:
|
||||
container_name: budi-nginx-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||
|
@ -38,7 +38,7 @@ services:
|
|||
couchdb-service:
|
||||
# platform: linux/amd64
|
||||
container_name: budi-couchdb-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: ibmcom/couchdb3
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
|
@ -59,7 +59,7 @@ services:
|
|||
|
||||
redis-service:
|
||||
container_name: budi-redis-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
|
|
|
@ -393,18 +393,45 @@ const getUrlBindings = asset => {
|
|||
|
||||
/**
|
||||
* 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
|
||||
const index = actions?.findIndex(action => action.id === actionId)
|
||||
if (index == null || index === -1) {
|
||||
return []
|
||||
return bindings
|
||||
}
|
||||
const prevActions = actions.slice(0, index)
|
||||
|
||||
// Generate bindings for any steps which provide context
|
||||
let bindings = []
|
||||
prevActions.forEach((action, idx) => {
|
||||
const def = ActionDefinitions.actions.find(
|
||||
x => x.name === action["##eventHandlerType"]
|
||||
|
@ -418,6 +445,7 @@ export const getButtonContextBindings = (actions, actionId) => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
|
|||
|
||||
export const getThemeStore = () => {
|
||||
const themeElement = document.documentElement
|
||||
|
||||
const initialValue = {
|
||||
theme: "darkest",
|
||||
options: ["lightest", "light", "dark", "darkest"],
|
||||
options: ["lightest", "light", "dark", "darkest", "nord"],
|
||||
}
|
||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||
|
||||
|
@ -21,6 +22,7 @@ export const getThemeStore = () => {
|
|||
`spectrum--${option}`,
|
||||
option === state.theme
|
||||
)
|
||||
themeElement.classList.add("spectrum--darkest")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
import { getAvailableActions } from "./index"
|
||||
import { generate } from "shortid"
|
||||
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
const actionTypes = getAvailableActions()
|
||||
|
||||
export let key
|
||||
export let actions
|
||||
export let bindings = []
|
||||
|
||||
|
@ -24,6 +26,9 @@
|
|||
|
||||
// These are ephemeral bindings which only exist while executing actions
|
||||
$: buttonContextBindings = getButtonContextBindings(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
key,
|
||||
actions,
|
||||
selectedAction?.id
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let key
|
||||
export let value = []
|
||||
export let name
|
||||
export let bindings
|
||||
|
@ -81,5 +82,6 @@
|
|||
bind:actions={tmpValue}
|
||||
eventType={name}
|
||||
{bindings}
|
||||
{key}
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
bindings={allBindings}
|
||||
name={key}
|
||||
text={label}
|
||||
{key}
|
||||
{type}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -2530,67 +2530,102 @@
|
|||
"styles": ["size"],
|
||||
"editable": true,
|
||||
"draggable": false,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
"label": "Provider",
|
||||
"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",
|
||||
"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",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Enable Location",
|
||||
"label": "Enable location",
|
||||
"key": "locationEnabled",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Enable Zoom",
|
||||
"label": "Enable zoom",
|
||||
"key": "zoomEnabled",
|
||||
"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",
|
||||
"label": "Tile URL",
|
||||
"key": "tileURL",
|
||||
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Default Location (lat,lng)",
|
||||
"label": "Default Location (when empty)",
|
||||
"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",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script context="module">
|
||||
// Cache the definition of settings for each component type
|
||||
let SettingsDefinitionCache = {}
|
||||
let SettingsDefinitionMapCache = {}
|
||||
|
||||
// Cache the settings of each component ID.
|
||||
// This speeds up remounting as well as repeaters.
|
||||
|
@ -74,6 +75,8 @@
|
|||
// Component information derived during initialisation
|
||||
let constructor
|
||||
let definition
|
||||
let settingsDefinition
|
||||
let settingsDefinitionMap
|
||||
|
||||
// Set up initial state for each new component instance
|
||||
$: initialise(instance)
|
||||
|
@ -118,7 +121,7 @@
|
|||
$: emptyState = empty && showEmptyState
|
||||
|
||||
// Enrich component settings
|
||||
$: enrichComponentSettings($context)
|
||||
$: enrichComponentSettings($context, settingsDefinitionMap)
|
||||
|
||||
// Evaluate conditional UI settings and store any component setting changes
|
||||
// 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
|
||||
let settingsDefinition
|
||||
if (SettingsDefinitionCache[definition.name]) {
|
||||
settingsDefinition = SettingsDefinitionCache[definition.name]
|
||||
settingsDefinitionMap = SettingsDefinitionMapCache[definition.name]
|
||||
} else {
|
||||
settingsDefinition = getSettingsDefinition(definition)
|
||||
settingsDefinitionMap = getSettingsDefinitionMap(settingsDefinition)
|
||||
SettingsDefinitionCache[definition.name] = settingsDefinition
|
||||
SettingsDefinitionMapCache[definition.name] = settingsDefinitionMap
|
||||
}
|
||||
|
||||
// Parse the instance settings, and cache them
|
||||
|
@ -190,7 +195,9 @@
|
|||
dynamicSettings = instanceSettings.dynamicSettings
|
||||
|
||||
// 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
|
||||
|
@ -226,6 +233,14 @@
|
|||
return settings
|
||||
}
|
||||
|
||||
const getSettingsDefinitionMap = settingsDefinition => {
|
||||
let map = {}
|
||||
settingsDefinition?.forEach(setting => {
|
||||
map[setting.key] = setting
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
const getInstanceSettings = (instance, settingsDefinition) => {
|
||||
// Get raw settings
|
||||
let settings = {}
|
||||
|
@ -248,7 +263,7 @@
|
|||
} else if (typeof value === "string" && value.includes("{{")) {
|
||||
// Strings can be trivially checked
|
||||
delete newStaticSettings[setting.key]
|
||||
} else if (value[0]?.["##eventHandlerType"] != null) {
|
||||
} else if (setting.type === "event") {
|
||||
// Always treat button actions as dynamic
|
||||
delete newStaticSettings[setting.key]
|
||||
} else if (typeof value === "object") {
|
||||
|
@ -273,7 +288,11 @@
|
|||
}
|
||||
|
||||
// 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
|
||||
if (!contextChanged && !options?.force) {
|
||||
return
|
||||
|
@ -285,7 +304,11 @@
|
|||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich settings with context
|
||||
const newEnrichedSettings = enrichProps(dynamicSettings, context)
|
||||
const newEnrichedSettings = enrichProps(
|
||||
dynamicSettings,
|
||||
context,
|
||||
settingsDefinitionMap
|
||||
)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import L from "leaflet"
|
||||
import sanitizeHtml from "sanitize-html"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import { Helpers, Button } from "@budibase/bbui"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import {
|
||||
FullScreenControl,
|
||||
LocationControl,
|
||||
|
@ -24,86 +24,16 @@
|
|||
export let defaultLocation
|
||||
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
export let mapAttribution
|
||||
export let creationEnabled = false
|
||||
export let onClickMarker
|
||||
export let onCreateMarker
|
||||
|
||||
const { styleable, notificationStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
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
|
||||
let locationControl = new LocationControl({
|
||||
const locationControl = new LocationControl({
|
||||
position: "bottomright",
|
||||
onLocationFail: err => {
|
||||
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
|
||||
|
@ -129,13 +59,135 @@
|
|||
}
|
||||
},
|
||||
})
|
||||
let fullScreenControl = new FullScreenControl({
|
||||
const fullScreenControl = new FullScreenControl({
|
||||
position: "topright",
|
||||
})
|
||||
let zoomControl = L.control.zoom({
|
||||
const zoomControl = L.control.zoom({
|
||||
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) => {
|
||||
if (typeof mapInstance !== "object") {
|
||||
return
|
||||
|
@ -171,44 +223,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
//Map icon and marker configuration
|
||||
const mapIconMarkup =
|
||||
'<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>'
|
||||
const mapIcon = L.divIcon({
|
||||
html: mapIconMarkup,
|
||||
className: "embedded-map-marker",
|
||||
iconSize: [26, 26],
|
||||
iconAnchor: [13, 26],
|
||||
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) {
|
||||
const addMapMarkers = (
|
||||
mapInstance,
|
||||
validRows,
|
||||
latKey,
|
||||
lngKey,
|
||||
titleKey,
|
||||
onClick
|
||||
) => {
|
||||
if (!mapInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
mapMarkerGroup.clearLayers()
|
||||
|
||||
const validRows = rows.filter(row => {
|
||||
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
|
||||
})
|
||||
if (!validRows?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
validRows.forEach(row => {
|
||||
let markerCoords = [row[latKey], row[lngKey]]
|
||||
|
||||
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
|
||||
let markerContent = generateMarkerPopupContent(
|
||||
row[latKey],
|
||||
|
@ -216,52 +249,105 @@
|
|||
row[titleKey]
|
||||
)
|
||||
|
||||
marker.bindPopup(markerContent).addTo(mapMarkerGroup)
|
||||
marker
|
||||
.bindTooltip(markerContent, {
|
||||
direction: "top",
|
||||
offset: [0, -25],
|
||||
})
|
||||
.addTo(mapMarkerGroup)
|
||||
|
||||
//https://github.com/Leaflet/Leaflet/issues/7331
|
||||
marker.on("click", function () {
|
||||
this.openPopup()
|
||||
})
|
||||
|
||||
mapMarkers = [...mapMarkers, marker]
|
||||
if (onClick) {
|
||||
marker.on("click", () => {
|
||||
onClick({
|
||||
marker: row,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Zoom to markers if this is the first time
|
||||
if (!initialMarkerZoomCompleted) {
|
||||
resetView()
|
||||
initialMarkerZoomCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
const generateMarkerPopupContent = (latitude, longitude, text) => {
|
||||
return text || latitude + "," + longitude
|
||||
}
|
||||
|
||||
const initMap = () => {
|
||||
const initCoords = defaultCoordinates
|
||||
|
||||
const initMap = (tileURL, attribution, zoom) => {
|
||||
if (!mounted) {
|
||||
return
|
||||
}
|
||||
if (mapInstance) {
|
||||
mapInstance.remove()
|
||||
}
|
||||
mapInstance = L.map(embeddedMapId, mapOptions)
|
||||
mapMarkerGroup.addTo(mapInstance)
|
||||
candidateMarkerGroup.addTo(mapInstance)
|
||||
|
||||
const cleanAttribution = sanitizeHtml(mapAttribution, {
|
||||
// Add attribution
|
||||
const cleanAttribution = sanitizeHtml(attribution, {
|
||||
allowedTags: ["a"],
|
||||
allowedAttributes: {
|
||||
a: ["href", "target"],
|
||||
},
|
||||
})
|
||||
|
||||
L.tileLayer(tileURL, {
|
||||
attribution: "© " + cleanAttribution,
|
||||
zoom: adjustedZoomLevel,
|
||||
zoom,
|
||||
}).addTo(mapInstance)
|
||||
|
||||
//Initialise the map
|
||||
mapInstance.setView(initCoords, adjustedZoomLevel)
|
||||
// Add click handler
|
||||
mapInstance.on("click", handleMapClick)
|
||||
|
||||
// Reset view
|
||||
resetView()
|
||||
}
|
||||
|
||||
const mapAction = () => {
|
||||
initMap()
|
||||
return {
|
||||
destroy() {
|
||||
mapInstance.remove()
|
||||
mapInstance = undefined
|
||||
},
|
||||
const handleMapClick = e => {
|
||||
if (!creationEnabled) {
|
||||
return
|
||||
}
|
||||
candidateMarkerGroup.clearLayers()
|
||||
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>
|
||||
|
||||
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
|
||||
|
@ -269,12 +355,20 @@
|
|||
<div>{error}</div>
|
||||
{/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>
|
||||
|
||||
<style>
|
||||
.embedded-map-wrapper {
|
||||
background-color: #f1f1f1;
|
||||
height: 320px;
|
||||
}
|
||||
.map-default {
|
||||
min-height: 180px;
|
||||
|
@ -287,6 +381,9 @@
|
|||
.embedded-map :global(.embedded-map-marker) {
|
||||
color: #ee3b35;
|
||||
}
|
||||
.embedded-map :global(.embedded-map-marker--candidate) {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
.embedded-map :global(.embedded-map-control) {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
@ -294,4 +391,12 @@
|
|||
height: 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>
|
||||
|
|
|
@ -4,30 +4,36 @@ const initialState = {
|
|||
showConfirmation: false,
|
||||
title: null,
|
||||
text: null,
|
||||
callback: null,
|
||||
onConfirm: null,
|
||||
onCancel: null,
|
||||
}
|
||||
|
||||
const createConfirmationStore = () => {
|
||||
const store = writable(initialState)
|
||||
|
||||
const showConfirmation = (title, text, callback) => {
|
||||
const showConfirmation = (title, text, onConfirm, onCancel) => {
|
||||
store.set({
|
||||
showConfirmation: true,
|
||||
title,
|
||||
text,
|
||||
callback,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
})
|
||||
}
|
||||
const confirm = async () => {
|
||||
const state = get(store)
|
||||
if (!state.showConfirmation || !state.callback) {
|
||||
if (!state.showConfirmation || !state.onConfirm) {
|
||||
return
|
||||
}
|
||||
store.set(initialState)
|
||||
await state.callback()
|
||||
await state.onConfirm()
|
||||
}
|
||||
const cancel = () => {
|
||||
const state = get(store)
|
||||
store.set(initialState)
|
||||
if (state.onCancel) {
|
||||
state.onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -308,7 +308,7 @@ export const enrichButtonActions = (actions, context) => {
|
|||
let buttonContext = context.actions || []
|
||||
|
||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||
return async () => {
|
||||
return async eventContext => {
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
try {
|
||||
// Skip any non-existent action definitions
|
||||
|
@ -317,7 +317,11 @@ export const enrichButtonActions = (actions, context) => {
|
|||
}
|
||||
|
||||
// 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
|
||||
let action = actions[i]
|
||||
|
@ -327,33 +331,36 @@ export const enrichButtonActions = (actions, context) => {
|
|||
// If this action is confirmable, show confirmation and await a
|
||||
// callback to execute further actions
|
||||
if (action.parameters?.confirm) {
|
||||
const defaultText = confirmTextMap[action["##eventHandlerType"]]
|
||||
const confirmText = action.parameters?.confirmText || defaultText
|
||||
confirmationStore.actions.showConfirmation(
|
||||
action["##eventHandlerType"],
|
||||
confirmText,
|
||||
async () => {
|
||||
// When confirmed, execute this action immediately,
|
||||
// then execute the rest of the actions in the chain
|
||||
const result = await callback()
|
||||
if (result !== false) {
|
||||
// Generate a new total context to pass into the next enrichment
|
||||
buttonContext.push(result)
|
||||
const newContext = { ...context, actions: buttonContext }
|
||||
return new Promise(resolve => {
|
||||
const defaultText = confirmTextMap[action["##eventHandlerType"]]
|
||||
const confirmText = action.parameters?.confirmText || defaultText
|
||||
confirmationStore.actions.showConfirmation(
|
||||
action["##eventHandlerType"],
|
||||
confirmText,
|
||||
async () => {
|
||||
// When confirmed, execute this action immediately,
|
||||
// then execute the rest of the actions in the chain
|
||||
const result = await callback()
|
||||
if (result !== false) {
|
||||
// Generate a new total context to pass into the next enrichment
|
||||
buttonContext.push(result)
|
||||
const newContext = { ...context, actions: buttonContext }
|
||||
|
||||
// Enrich and call the next button action
|
||||
const next = enrichButtonActions(
|
||||
actions.slice(i + 1),
|
||||
newContext
|
||||
)
|
||||
await next()
|
||||
// Enrich and call the next button action
|
||||
const next = enrichButtonActions(
|
||||
actions.slice(i + 1),
|
||||
newContext
|
||||
)
|
||||
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
|
||||
|
|
|
@ -22,7 +22,7 @@ export const propsAreSame = (a, b) => {
|
|||
* Enriches component props.
|
||||
* 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
|
||||
// Duplicate the closest context as "data" which the builder requires
|
||||
const totalContext = {
|
||||
|
@ -38,7 +38,7 @@ export const enrichProps = (props, context) => {
|
|||
let normalProps = { ...props }
|
||||
let actionProps = {}
|
||||
Object.keys(normalProps).forEach(prop => {
|
||||
if (prop?.toLowerCase().includes("onclick")) {
|
||||
if (settingsDefinitionMap?.[prop]?.type === "event") {
|
||||
actionProps[prop] = normalProps[prop]
|
||||
delete normalProps[prop]
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export const enrichProps = (props, context) => {
|
|||
// Conditions
|
||||
if (enrichedProps._conditions?.length) {
|
||||
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
|
||||
// action
|
||||
condition.settingValue = enrichButtonActions(
|
||||
|
|
Loading…
Reference in New Issue