diff --git a/Gemfile b/Gemfile index 9d14cb216f..6632aa3f84 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index 890bb9dbe4..6e2c989d94 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -796,6 +800,7 @@ DEPENDENCIES govuk_web_banners htmlentities i18n-coverage + importmap-rails (~> 2.2) listen pact (~> 1.67) pact_broker-client diff --git a/app/assets/javascripts/components/map.js b/app/assets/javascripts/components/map.js index 44ad076924..fb2bc816f5 100644 --- a/app/assets/javascripts/components/map.js +++ b/app/assets/javascripts/components/map.js @@ -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) { @@ -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, @@ -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', () => { diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000000..beff742ec3 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails diff --git a/app/javascript/components/map/asyncToGenerator.js b/app/javascript/components/map/asyncToGenerator.js new file mode 100644 index 0000000000..bce197e5ff --- /dev/null +++ b/app/javascript/components/map/asyncToGenerator.js @@ -0,0 +1 @@ +//= require @babel/runtime-corejs3/helpers/esm/asyncToGenerator.js diff --git a/app/javascript/components/map/defineProperty.js b/app/javascript/components/map/defineProperty.js new file mode 100644 index 0000000000..b161eb8823 --- /dev/null +++ b/app/javascript/components/map/defineProperty.js @@ -0,0 +1 @@ +//= require @babel/runtime/helpers/defineProperty.js diff --git a/app/javascript/components/map/im-core.js b/app/javascript/components/map/im-core.js new file mode 100644 index 0000000000..ccb1043ad7 --- /dev/null +++ b/app/javascript/components/map/im-core.js @@ -0,0 +1 @@ +//= require @defra/interactive-map/dist/esm/im-core.js diff --git a/app/javascript/components/map/im-shell.js b/app/javascript/components/map/im-shell.js new file mode 100644 index 0000000000..db9ad7afa8 --- /dev/null +++ b/app/javascript/components/map/im-shell.js @@ -0,0 +1 @@ +//= require @defra/interactive-map/dist/esm/im-shell.js diff --git a/app/javascript/components/map/index.js b/app/javascript/components/map/index.js new file mode 100644 index 0000000000..085dc92e45 --- /dev/null +++ b/app/javascript/components/map/index.js @@ -0,0 +1 @@ +//= require @defra/interactive-map/dist/esm/index.js diff --git a/app/javascript/components/map/map.js b/app/javascript/components/map/map.js new file mode 100644 index 0000000000..4fcd0d309c --- /dev/null +++ b/app/javascript/components/map/map.js @@ -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}` + 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(' { + 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) diff --git a/app/javascript/components/map/maplibre.js b/app/javascript/components/map/maplibre.js new file mode 100644 index 0000000000..54434292c8 --- /dev/null +++ b/app/javascript/components/map/maplibre.js @@ -0,0 +1,2 @@ +//= require @defra/interactive-map/providers/maplibre/dist/esm/index.js +// im-maplibre-provider.js diff --git a/app/javascript/components/map/objectWithoutProperties.js b/app/javascript/components/map/objectWithoutProperties.js new file mode 100644 index 0000000000..78139782f0 --- /dev/null +++ b/app/javascript/components/map/objectWithoutProperties.js @@ -0,0 +1 @@ +//= require @babel/runtime/helpers/objectWithoutProperties.js diff --git a/app/views/components/_map.html.erb b/app/views/components/_map.html.erb index 0e3b1afc13..60d27a7d65 100644 --- a/app/views/components/_map.html.erb +++ b/app/views/components/_map.html.erb @@ -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 %> diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..b779bb4255 --- /dev/null +++ b/config/importmap.rb @@ -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" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index ded865c00f..4f4a787441 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -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 ) diff --git a/package.json b/package.json index 86aea1d972..a980fcdf2d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/yarn.lock b/yarn.lock index 532c40027f..d8fbd48c07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"