Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ gem "govuk_personalisation"
gem "govuk_publishing_components"
gem "govuk_web_banners"
gem "htmlentities"
gem "importmap-rails", "~> 2.2"
gem "plek"
gem "rack-utf8_sanitizer"
gem "rails-i18n"
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
importmap-rails (2.2.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.9.2)
irb (1.18.0)
pp (>= 0.6.0)
Expand Down Expand Up @@ -796,6 +800,7 @@ DEPENDENCIES
govuk_web_banners
htmlentities
i18n-coverage
importmap-rails (~> 2.2)
listen
pact (~> 1.67)
pact_broker-client
Expand Down
7 changes: 5 additions & 2 deletions app/assets/javascripts/components/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

import InteractiveMap from '@defra/interactive-map'
import maplibreProvider from '@defra/interactive-map/providers/maplibre'

(function (Modules) {
class Map {
constructor ($module) {
Expand All @@ -15,7 +18,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
})

const config = {
mapProvider: defra.maplibreProvider({ workerUrl: cspWorker }),
mapProvider: maplibreProvider({ workerUrl: cspWorker }),
behaviour: 'inline',
mapStyle: {
url: window.GOVUK.mapComponentStyles,
Expand Down Expand Up @@ -54,7 +57,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
this.map_element.setAttribute('id', id)
this.map_element.classList.add('app-c-map--enabled')

this.map = new defra.InteractiveMap(this.map_id, this.config)
this.map = new InteractiveMap(this.map_id, this.config)

/* istanbul ignore next */
this.map.on('map:ready', () => {
Expand Down
1 change: 1 addition & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
1 change: 1 addition & 0 deletions app/javascript/components/map/asyncToGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @babel/runtime-corejs3/helpers/esm/asyncToGenerator.js
1 change: 1 addition & 0 deletions app/javascript/components/map/defineProperty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @babel/runtime/helpers/defineProperty.js
1 change: 1 addition & 0 deletions app/javascript/components/map/im-core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @defra/interactive-map/dist/esm/im-core.js
1 change: 1 addition & 0 deletions app/javascript/components/map/im-shell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @defra/interactive-map/dist/esm/im-shell.js
1 change: 1 addition & 0 deletions app/javascript/components/map/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @defra/interactive-map/dist/esm/index.js
196 changes: 196 additions & 0 deletions app/javascript/components/map/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/* global defra */
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

import InteractiveMap from '@defra/interactive-map'
import maplibreProvider from '@defra/interactive-map/providers/maplibre'

(function (Modules) {
class Map {
constructor ($module) {
console.log('constructor')
this.$module = $module
this.map_element = this.$module.querySelector('.app-c-map')
this.map_id = this.$module.getAttribute('id')
const cspWorker = this.$module.getAttribute('data-csp-worker')

this.interactPlugin = defra.interactPlugin({
deselectOnClickOutside: true
})

const config = {
mapProvider: maplibreProvider({ workerUrl: cspWorker }),
behaviour: 'inline',
mapStyle: {
url: window.GOVUK.mapComponentStyles,
backgroundColor: '#f5f5f0'
},
plugins: [this.interactPlugin],
urlPosition: 'none',
minZoom: 4,
maxZoom: 16,
center: [-0.09, 51.505],
containerHeight: '500px'
}
const passedConfig = JSON.parse(this.$module.getAttribute('data-config')) || {}
this.config = Object.assign(config, passedConfig)

this.markers = JSON.parse(this.$module.getAttribute('data-markers')) || []
this.geoJsonUrl = this.$module.getAttribute('data-geojson')
if (this.geoJsonUrl && !this.geoJsonUrl.startsWith('/')) {
console.error(`Error: external URLs for geoJSON are not allowed: ${this.geoJsonUrl}`)
this.geoJsonUrl = false
}
this.headingLevel = parseInt(this.$module.getAttribute('data-heading-level')) || 2

this.markerOptions = {
symbol: 'circle',
backgroundColor: '#1d70b8',
foregroundColor: '#FFFFFF',
haloWidth: 3,
selectedWidth: 8
}
}

init () {
const id = this.$module.getAttribute('id')
this.$module.setAttribute('id', '')
this.map_element.setAttribute('id', id)
this.map_element.classList.add('app-c-map--enabled')

this.map = new InteractiveMap(this.map_id, this.config)

/* istanbul ignore next */
this.map.on('map:ready', () => {
this.addAllMarkers()
})

/* istanbul ignore next */
this.map.on('interact:selectionchange', (e) => {
if (e.selectedMarkers.length > 0) {
let marker = parseInt(e.selectedMarkers[0].replace('marker-', ''))
marker = this.markers[marker]
this.map.addPanel('the-panel', {
focus: false,
label: marker.name,
html: this.createPopupContent(marker),
mobile: { slot: 'drawer', dismissible: true },
tablet: { slot: 'left-top', dismissible: true, width: '280px' },
desktop: { slot: 'left-top', dismissible: true, width: '280px' }
})
} else {
this.map.hidePanel('the-panel')
}
})

/* istanbul ignore next */
this.map.on('app:panelclosed', (e) => {
this.interactPlugin.clear()
})
}

createPopupContent (feature) {
const heading = `h${this.headingLevel}`
let popupContent = `<${heading} class="govuk-heading-s govuk-!-margin-bottom-2">${feature.properties.name}</${heading}>`
if (feature.properties.description) {
popupContent = `${popupContent} ${feature.properties.description}`
}
popupContent = this.removeScript(popupContent)
return popupContent
}

removeScript (input) {
do {
input = input.replace(/<[\s]*script/g, '')
} while (input.includes('<script'))
return input
}

async addAllMarkers () {
if (this.geoJsonUrl) {
try {
const response = await fetch(this.geoJsonUrl)
if (!response.ok) {
throw new Error(`Response status: ${response.status}`)
}
const result = await response.json()
if (this.markers) {
this.markers = this.markers.concat(result.features)
}
} catch (error) {
console.error(`${error}, with geojson at ${this.geoJsonUrl}`)
}
}
this.markers.sort((a, b) => {
const nameA = a.properties.name.toUpperCase()
const nameB = b.properties.name.toUpperCase()
if (nameA < nameB) {
return -1
}
if (nameA > nameB) {
return 1
}
return 0 // names are equal
})
this.addMarkers()

// only fit to bounds if there are more than one markers
if (this.markers.length > 0 && !this.config.bounds) {
this.map.fitToBounds({
type: 'FeatureCollection',
features: this.markers
})
}
if (this.markers.length > 0) {
this.interactPlugin.enable()
this.addPopupsList()
}
}

addMarkers () {
const allowedColours = {
blue: '#1d70b8',
green: '#0f7a52',
orange: '#f47738',
red: '#ca3535'
}
const allowedSymbols = ['circle', 'pin', 'square']
this.markers.forEach((marker, index) => {
if (marker.marker) {
const colour = marker.marker.colour || false
if (colour) {
if (allowedColours[colour]) {
marker.marker.backgroundColor = allowedColours[colour]
}
delete marker.marker.colour
}
const symbol = marker.marker.symbol || false
if (!(symbol && allowedSymbols.includes(symbol))) {
delete marker.marker.symbol
}
}
const options = Object.assign(Object.assign({}, this.markerOptions), marker.marker || {})
this.map.addMarker(`marker-${index}`, marker.geometry.coordinates, options)
})
}

addPopupsList () {
const popupsListWrapper = this.$module.querySelector('.app-c-map__markers-list')
const popupsListEl = this.$module.querySelector('.js-list-markers ol')

if (popupsListWrapper && popupsListEl) {
popupsListWrapper.classList.add('app-c-map__markers-list--visible')
const popupsList = []
this.markers.forEach(marker => {
popupsList.push(this.createPopupContent(marker))
})
popupsList.forEach(popup => {
const listItem = document.createElement('li')
listItem.innerHTML = popup
popupsListEl.appendChild(listItem)
})
}
}
}
Modules.Map = Map
})(window.GOVUK.Modules)
2 changes: 2 additions & 0 deletions app/javascript/components/map/maplibre.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//= require @defra/interactive-map/providers/maplibre/dist/esm/index.js
// im-maplibre-provider.js
1 change: 1 addition & 0 deletions app/javascript/components/map/objectWithoutProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require @babel/runtime/helpers/objectWithoutProperties.js
5 changes: 2 additions & 3 deletions app/views/components/_map.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@
<% end %>

<% unless @include_script[:script_included] %>
<%= javascript_include_tag "components/map/defra-map.js", integrity: false %>
<%= javascript_include_tag "components/map.js", integrity: false, type: "module" %>
<%= javascript_include_tag "components/map/map-style-liberty.js", integrity: false %>
<%= javascript_importmap_tags %>
<%= javascript_import_module_tag "components/map/map" %>
<% @include_script[:script_included] = true %>
<% end %>

Expand Down
4 changes: 4 additions & 0 deletions bin/importmap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby

require_relative "../config/application"
require "importmap/commands"
22 changes: 22 additions & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Pin npm packages by running ./bin/importmap

# Pin the default entry point required by javascript_importmap_tags
pin "application", preload: true
pin "components/map/map", preload: true
pin "components/map/index", preload: true
pin "components/map/im-core", preload: true
pin "components/map/im-shell", preload: true
pin "components/map/maplibre", preload: true

pin "@defra/interactive-map", to: "components/map/index.js"
pin "@defra/interactive-map/providers/maplibre", to: "components/map/maplibre.js"
# pin "@defra/interactive-map/plugins/interact", to: "./node_modules/@defra/interactive-map/plugins/interact/dist/esm/index.js"

pin "components/map/asyncToGenerator", preload: true
pin "components/map/defineProperty", preload: true
pin "components/map/objectWithoutProperties", preload: true

# try to avoid errors relating to babel, which is referenced by the defra ESM code
pin "@babel/runtime/helpers/asyncToGenerator", to: "components/map/asyncToGenerator.js"
pin "@babel/runtime/helpers/defineProperty", to: "components/map/defineProperty.js"
pin "@babel/runtime/helpers/objectWithoutProperties", to: "components/map/objectWithoutProperties.js"
12 changes: 12 additions & 0 deletions config/initializers/assets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@
# Add additional assets to the asset load path.
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join("node_modules")

# Precompile map.js independently so it can be fetched directly by the importmap
Rails.application.config.assets.precompile += %w( components/map/map.js )

Rails.application.config.assets.precompile += %w( components/map/index.js )
Rails.application.config.assets.precompile += %w( components/map/im-core.js )
Rails.application.config.assets.precompile += %w( components/map/im-shell.js )
Rails.application.config.assets.precompile += %w( components/map/maplibre.js )

Rails.application.config.assets.precompile += %w( components/map/asyncToGenerator.js )
Rails.application.config.assets.precompile += %w( components/map/defineProperty.js )
Rails.application.config.assets.precompile += %w( components/map/objectWithoutProperties.js )
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"packageManager": "yarn@4.18.0",
"dependencies": {
"@babel/runtime-corejs3": "^8.0.5",
"@defra/interactive-map": "0.0.30-alpha",
"leaflet": "^1.9.4",
"maplibre-gl": "5.24.0"
Expand Down
Empty file added vendor/javascript/.keep
Empty file.
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime-corejs3@npm:^8.0.5":
version: 8.0.5
resolution: "@babel/runtime-corejs3@npm:8.0.5"
dependencies:
core-js-pure: "npm:^3.48.0"
checksum: 10/5f8e90a52a45ce386a34af2b788aefd76ba34d12c5fa5e345c0ac3b71ab60d1d844b0a9634c38f7114a8530e15a096c133d633fbdd946e335bc9d775a51b9442
languageName: node
linkType: hard

"@babel/runtime@npm:^7.28.6":
version: 7.29.2
resolution: "@babel/runtime@npm:7.29.2"
Expand Down Expand Up @@ -1643,6 +1652,13 @@ __metadata:
languageName: node
linkType: hard

"core-js-pure@npm:^3.48.0":
version: 3.50.0
resolution: "core-js-pure@npm:3.50.0"
checksum: 10/4009d549c955dbd4274b58a6c36c7afce8a8f468a631bacb06708873bf503b1c91cbf5baa88c8605c2fff2ccc481fb337e9358c16c029c701d0fffa074e1706e
languageName: node
linkType: hard

"core-util-is@npm:~1.0.0":
version: 1.0.3
resolution: "core-util-is@npm:1.0.3"
Expand Down Expand Up @@ -2595,6 +2611,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "frontend@workspace:."
dependencies:
"@babel/runtime-corejs3": "npm:^8.0.5"
"@defra/interactive-map": "npm:0.0.30-alpha"
jasmine-browser-runner: "npm:^5.0.0"
jasmine-core: "npm:^7.0.2"
Expand Down