diff --git a/src/js/_ui/_ui.carousel.js b/src/js/_ui/_ui.carousel.js index 3cd1122..bc5c00f 100644 --- a/src/js/_ui/_ui.carousel.js +++ b/src/js/_ui/_ui.carousel.js @@ -2,80 +2,82 @@ import Events from '../_events'; import Carousel from 'bootstrap/js/src/carousel'; const CarouselUI = ((window) => { - const NAME = 'js-carousel'; + const NAME = 'js-carousel'; - const init = () => { - console.log(`${NAME}: init`); - document.querySelectorAll(`.${NAME}`).forEach((el, i) => { - const carousel = new Carousel(el); - // create next/prev arrows - if (el.dataset.bsArrows) { - const next = document.createElement('button'); - next.classList.add('carousel-control-next'); - next.setAttribute('type', 'button'); - next.setAttribute('data-bs-target', el.getAttribute('id')); - next.setAttribute('data-bs-slide', 'next'); - next.addEventListener('click', (e) => { - carousel.next(); + const init = () => { + console.log(`${NAME}: init`); + + document.querySelectorAll(`.${NAME}`).forEach((el, i) => { + const carousel = new Carousel(el); + // create next/prev arrows + if (el.dataset.bsArrows) { + const next = document.createElement('button'); + next.classList.add('carousel-control-next'); + next.setAttribute('type', 'button'); + next.setAttribute('data-bs-target', el.getAttribute('id')); + next.setAttribute('data-bs-slide', 'next'); + next.addEventListener('click', (e) => { + carousel.next(); + }); + next.innerHTML = 'Next'; + el.appendChild(next); + + const prev = document.createElement('button'); + prev.setAttribute('type', 'button'); + prev.classList.add('carousel-control-prev'); + prev.setAttribute('data-bs-target', el.getAttribute('id')); + prev.setAttribute('data-bs-slide', 'prev'); + prev.addEventListener('click', (e) => { + carousel.prev(); + }); + prev.innerHTML = 'Previous'; + el.appendChild(prev); + } + + if (el.dataset.bsIndicators) { + const indicators = document.createElement('div'); + indicators.classList.add('carousel-indicators'); + const items = el.querySelectorAll('.carousel-item'); + let i = 0; + while (i < items.length) { + const ind = document.createElement('button'); + ind.setAttribute('type', 'button'); + if (i == 0) { + ind.classList.add('active'); + } + ind.setAttribute('data-bs-target', el.getAttribute('id')); + ind.setAttribute('data-bs-slide-to', i); + + ind.addEventListener('click', (e) => { + const target = e.target; + carousel.to(target.getAttribute('data-bs-slide-to')); + indicators.querySelectorAll('.active').forEach((ind2) => { + ind2.classList.remove('active'); + }); + target.classList.add('active'); + }); + + indicators.appendChild(ind); + i++; + } + + el.appendChild(indicators); + el.addEventListener('slide.bs.carousel', (e) => { + el.querySelectorAll('.carousel-indicators .active').forEach((ind2) => { + ind2.classList.remove('active'); + }); + el.querySelectorAll(`.carousel-indicators [data-bs-slide-to="${ e.to }"]`).forEach((ind2) => { + ind2.classList.add('active'); + }); + }); + + } + el.classList.add(`${NAME}-active`); }); - next.innerHTML = 'Next'; - el.appendChild(next); + }; - const prev = document.createElement('button'); - prev.setAttribute('type', 'button'); - prev.classList.add('carousel-control-prev'); - prev.setAttribute('data-bs-target', el.getAttribute('id')); - prev.setAttribute('data-bs-slide', 'prev'); - prev.addEventListener('click', (e) => { - carousel.prev(); - }); - prev.innerHTML = 'Previous'; - el.appendChild(prev); - } - - if (el.dataset.bsIndicators) { - const indicators = document.createElement('div'); - indicators.classList.add('carousel-indicators'); - const items = el.querySelectorAll('.carousel-item'); - let i = 0; - while (i < items.length) { - const ind = document.createElement('button'); - ind.setAttribute('type', 'button'); - if (i == 0) { - ind.classList.add('active'); - } - ind.setAttribute('data-bs-target', el.getAttribute('id')); - ind.setAttribute('data-bs-slide-to', i); - - ind.addEventListener('click', (e) => { - const target = e.target; - carousel.to(target.getAttribute('data-bs-slide-to')); - indicators.querySelectorAll('.active').forEach((ind2) => { - ind2.classList.remove('active'); - }); - target.classList.add('active'); - }); - - indicators.appendChild(ind); - i++; - } - - el.appendChild(indicators); - el.addEventListener('slide.bs.carousel', (e) => { - el.querySelectorAll('.carousel-indicators .active').forEach((ind2) => { - ind2.classList.remove('active'); - }); - el.querySelectorAll(`.carousel-indicators [data-bs-slide-to="${ e.to }"]`).forEach((ind2) => { - ind2.classList.add('active'); - }); - }); - - } - el.classList.add(`${NAME}-active`); - }); - }; - - window.addEventListener(`${Events.LODEDANDREADY}`, init); - window.addEventListener(`${Events.AJAX}`, init); + window.addEventListener(`${Events.LODEDANDREADY}`, init); + window.addEventListener(`${Events.AJAX}`, init); })(window); + export default CarouselUI; diff --git a/src/js/_ui/map.api.js b/src/js/_ui/map.api.js new file mode 100644 index 0000000..465ece7 --- /dev/null +++ b/src/js/_ui/map.api.js @@ -0,0 +1,123 @@ +'use strict'; + +import Events from '../_events'; + +import '../../scss/_ui/map.api.scss'; + +import CONSTS from 'js/_consts'; + +const MapAPI = ((window) => { + // Constants + const NAME = 'js-mapapi'; + const MAP_DRIVER = CONSTS['MAP_DRIVER']; + + class MapAPI { + // Constructor + constructor(el) { + const ui = this; + const Drv = new MAP_DRIVER(); + const BODY = document.querySelector('body'); + const config = el.dataset; + config['center'] = [ + config['lng'] ? config['lng'] : BODY.dataset['default-lng'], + config['lat'] ? config['lat'] : BODY.dataset['default-lat'], + ]; + + /*config['style'] = config['style'] ? + jQuery.parseJSON(config['style']) : + null; + + config['font-family'] = $BODY.css('font-family');*/ + + if (!config['icon']) { + config['icon'] = ''; + } + + console.log(`${NAME}: init ${Drv.getName()}...`); + ui.drv = Drv; + ui.el = el; + ui.config = config; + + Drv.init(el, config); + + el.addEventListener(Events.MAPAPILOADED, () => { + ui.addMarkers() + }); + } + + // Public methods + getMap() { + return ui.map; + } + + dispose() { + const ui = this; + + ui.el = null; + ui.el.classList.remove(`${NAME}-active`); + } + + addMarkers() { + console.log(`${NAME}: addMarkers`); + const ui = this; + const el = ui.el; + const Drv = ui.drv; + const config = ui.config; + + ui.map = Drv.getMap(); + + if (config['geojson']) { + console.log(`${NAME}: setting up geocode data`); + Drv.addGeoJson(config); + } else if (config['address']) { + console.log(config['address']); + console.log(`${NAME}: setting up address marker`); + Drv.geocode(config['address'], (results) => { + console.log(results); + + const lat = results[0].geometry.location.lat(); + const lng = results[0].geometry.location.lng(); + + console.log( + `${NAME}: setting up single lat/lng marker lat: ${lat} lng: ${lng}`, + ); + + Drv.addMarker([lng, lat], config); + ui.map.setCenter({ + lat, + lng + }); + }); + } else if (config['lat'] && config['lng']) { + const lat = config['lat']; + const lng = config['lng']; + + console.log( + `${NAME}: setting up single lat/lng marker lat: ${lat} lng: ${lng}`, + ); + + Drv.addMarker([lng, lat], config); + } + + el.classList.add(`${NAME}-active`); + + el.dispatchEvent(new Event(Events.MAPLOADED)); + console.log(`${NAME}: Map is loaded`); + } + } + + const init = () => { + console.log(`${NAME}: init`); + document.querySelectorAll(`.${NAME}`).forEach((el, i) => { + const map = new MapAPI(el); + }); + } + + // auto-apply + window.addEventListener(`${Events.LODEDANDREADY}`, init); + window.addEventListener(`${Events.AJAX}`, init); + + return MapAPI; +})(window); + +export default MapAPI; diff --git a/src/js/drivers/_google.track.external.links.js b/src/js/drivers/_google.track.external.links.js new file mode 100644 index 0000000..1ba472b --- /dev/null +++ b/src/js/drivers/_google.track.external.links.js @@ -0,0 +1,61 @@ +function _gaLt(event) { + if (typeof ga !== 'function') { + return; + } + + var el = event.srcElement || event.target; + + /* Loop up the DOM tree through parent elements if clicked element is not a link (eg: an image inside a link) */ + while ( + el && + (typeof el.tagName == 'undefined' || + el.tagName.toLowerCase() != 'a' || + !el.href) + ) { + el = el.parentNode; + } + + if (el && el.href) { + /* link */ + var link = el.href; + if (link.indexOf(location.host) == -1 && !link.match(/^javascript:/i)) { + /* external link */ + /* HitCallback function to either open link in either same or new window */ + var hitBack = function(link, target) { + target ? window.open(link, target) : (window.location.href = link); + }; + /* Is target set and not _(self|parent|top)? */ + var target = + el.target && !el.target.match(/^_(self|parent|top)$/i) + ? el.target + : false; + /* send event with callback */ + ga( + 'send', + 'event', + 'Outgoing Links', + link, + document.location.pathname + document.location.search, + { hitCallback: hitBack(link, target) }, + ); + + /* Prevent standard click */ + event.preventDefault ? event.preventDefault() : (event.returnValue = !1); + } + } +} + +/* Attach the event to all clicks in the document after page has loaded */ +var w = window; +w.addEventListener + ? w.addEventListener( + 'load', + () => { + document.body.addEventListener('click', _gaLt, !1); + }, + !1, + ) + : w.attachEvent && + w.attachEvent('onload', () => { + document.body.attachEvent('onclick', _gaLt); + }); diff --git a/src/js/drivers/_map.google.font-icons.js b/src/js/drivers/_map.google.font-icons.js new file mode 100644 index 0000000..ccb5373 --- /dev/null +++ b/src/js/drivers/_map.google.font-icons.js @@ -0,0 +1,199 @@ +'use strict'; + +const Obj = { + init: () => { + class GoogleMapsHtmlOverlay extends google.maps.OverlayView { + constructor(options) { + super(); + const ui = this; + + ui.setMap(options.map); + ui.position = options.position; + ui.html = + (options.html ? + options.html : + '
' + ); + ui.divClass = options.divClass; + ui.align = options.align; + ui.isDebugMode = options.debug; + ui.onClick = options.onClick; + ui.onMouseOver = options.onMouseOver; + + ui.isBoolean = (arg) => { + if (typeof arg === 'boolean') { + return true; + } else { + return false; + } + }; + + ui.isNotUndefined = (arg) => { + if (typeof arg !== 'undefined') { + return true; + } else { + return false; + } + }; + + ui.hasContent = (arg) => { + if (arg.length > 0) { + return true; + } else { + return false; + } + }; + + ui.isString = (arg) => { + if (typeof arg === 'string') { + return true; + } else { + return false; + } + }; + + ui.isFunction = (arg) => { + if (typeof arg === 'function') { + return true; + } else { + return false; + } + }; + } + onAdd() { + const ui = this; + + // Create div element. + ui.div = document.createElement('div'); + ui.div.style.position = 'absolute'; + + // Validate and set custom div class + if (ui.isNotUndefined(ui.divClass) && ui.hasContent(ui.divClass)) + ui.div.className = ui.divClass; + + // Validate and set custom HTML + if ( + ui.isNotUndefined(ui.html) && + ui.hasContent(ui.html) && + ui.isString(ui.html) + ) + ui.div.innerHTML = ui.html; + + // If debug mode is enabled custom content will be replaced with debug content + if (ui.isBoolean(ui.isDebugMode) && ui.isDebugMode) { + ui.div.className = 'debug-mode'; + ui.div.innerHTML = + '
' + + '
Debug mode
'; + ui.div.setAttribute( + 'style', + 'position: absolute;' + + 'border: 5px dashed red;' + + 'height: 150px;' + + 'width: 150px;' + + 'display: flex;' + + 'justify-content: center;' + + 'align-items: center;' + ); + } + + // Add element to clickable layer + ui.getPanes().overlayMouseTarget.appendChild(ui.div); + + // Add listeners to the element. + google.maps.event.addDomListener(ui.div, 'click', (event) => { + google.maps.event.trigger(ui, 'click'); + if (ui.isFunction(ui.onClick)) ui.onClick(); + event.stopPropagation(); + }); + + google.maps.event.addDomListener(ui.div, 'mouseover', (event) => { + google.maps.event.trigger(ui, 'mouseover'); + if (ui.isFunction(ui.onMouseOver)) ui.onMouseOver(); + event.stopPropagation(); + }); + } + + draw() { + const ui = this; + + // Calculate position of div + var positionInPixels = ui.getProjection().fromLatLngToDivPixel( + new google.maps.LatLng(ui.position) + ); + + // Align HTML overlay relative to original position + var divOffset = { + y: undefined, + x: undefined, + }; + + switch (Array.isArray(ui.align) ? ui.align.join(' ') : '') { + case 'left top': + divOffset.y = ui.div.offsetHeight; + divOffset.x = ui.div.offsetWidth; + break; + case 'left center': + divOffset.y = ui.div.offsetHeight / 2; + divOffset.x = ui.div.offsetWidth; + break; + case 'left bottom': + divOffset.y = 0; + divOffset.x = ui.div.offsetWidth; + break; + case 'center top': + divOffset.y = ui.div.offsetHeight; + divOffset.x = ui.div.offsetWidth / 2; + break; + case 'center center': + divOffset.y = ui.div.offsetHeight / 2; + divOffset.x = ui.div.offsetWidth / 2; + break; + case 'center bottom': + divOffset.y = 0; + divOffset.x = ui.div.offsetWidth / 2; + break; + case 'right top': + divOffset.y = ui.div.offsetHeight; + divOffset.x = 0; + break; + case 'right center': + divOffset.y = ui.div.offsetHeight / 2; + divOffset.x = 0; + break; + case 'right bottom': + divOffset.y = 0; + divOffset.x = 0; + break; + default: + divOffset.y = ui.div.offsetHeight / 2; + divOffset.x = ui.div.offsetWidth / 2; + } + + // Set position + ui.div.style.top = `${positionInPixels.y - divOffset.y }px`; + ui.div.style.left = `${positionInPixels.x - divOffset.x }px`; + } + + getPosition() { + const ui = this; + return ui.position; + } + + getDiv() { + const ui = this; + return ui.div; + } + + setPosition(position, align) { + const ui = this; + ui.position = position; + ui.align = align; + ui.draw(); + } + } + return GoogleMapsHtmlOverlay; + }, +} + +export default Obj; diff --git a/src/js/drivers/_map.google.js b/src/js/drivers/_map.google.js new file mode 100644 index 0000000..8ca290a --- /dev/null +++ b/src/js/drivers/_map.google.js @@ -0,0 +1,283 @@ +'use strict'; + +import MarkerClusterer from '@googlemaps/markerclustererplus'; + +import Events from '../_events'; +import MarkerUI from './_map.google.marker'; + +const GoogleMapsDriver = ((window) => { + class GoogleMapsDriver { + getName() { + return 'GoogleMapsDriver'; + } + + init(el, config = []) { + const ui = this; + + ui.el = el; + ui.config = config; + ui.markers = []; + + window[`init${ui.getName()}`] = () => { + ui.googleApiLoaded(); + }; + + const script = document.createElement('script'); + script.src = `https://maps.googleapis.com/maps/api/js?key=${config['key']}&callback=init${ui.getName()}`; + script.async = true; + script.defer = true; + document.head.appendChild(script); + } + + googleApiLoaded() { + const ui = this; + + const el = ui.el; + const config = ui.config; + const mapDiv = el.querySelector('.mapAPI-map'); + const zoom = config['mapZoom'] && config['mapZoom'] !== '0' ? config['mapZoom'] : 10; + const center = config['center'] && config['center'] !== ',' ? + { + lat: config['center'][1], + lng: config['center'][0], + } : + { + lat: 0, + lng: 0, + }; + const style = config['style'] ? config['style'] : null; + + console.log(`${ui.getName()}: API is loaded`); + // init fontawesome icons + ui.MarkerUI = MarkerUI.init(); + + ui.map = new google.maps.Map(mapDiv, { + zoom, + center, + fullscreenControl: true, + styles: style, + }); + + ui.default_zoom = zoom; + + mapDiv.classList.add('mapboxgl-map'); + + ui.popup = new ui.MarkerUI({ + map: ui.map, + align: ['center', 'top'], + divClass: 'mapboxgl-popup popup mapboxgl-popup-anchor-bottom d-none', + html: '
' + + '
×
' + + '
' + + '
', + }); + ui.popup.setMap(ui.map); + + ui.geocoder = new google.maps.Geocoder(); + + ui.cluster = new MarkerClusterer(ui.map, null, { + styles: [{ + width: 30, + height: 30, + className: 'mapboxgl-cluster', + }], + }); + + el.dispatchEvent(new Event(Events.MAPAPILOADED)); + } + + addMarker(crds, config) { + const ui = this; + + const pos = { + lat: crds[1], + lng: crds[0], + }; + + const marker = new ui.MarkerUI({ + position: pos, + map: ui.map, + align: ['center', 'top'], + html: `
${config['icon']}
`, + onClick: () => { + const el = document.querySelector(`#Marker${config['id']}`); + ui.showPopup(pos, config['content']); + + el.dispatchEvent(new Event(Events.MAPMARKERCLICK)); + }, + }); + + ui.markers.push(marker); + + ui.cluster.addMarker(marker); + + return marker; + } + + showPopup(pos, content) { + const ui = this; + const popup = ui.popup.getDiv(); + + if (ui.config['flyToMarker']) { + ui.map.setCenter(pos); // panTo + if (!ui.config['noZoom']) { + ui.map.setZoom(18); + } + } + + // keep it hidden to render content + popup.style.opacity = '0'; + popup.classList.remove('d-none'); + + popup.querySelector('.mapboxgl-popup-content .html').innerHTML = content; + + popup.querySelector('.mapboxgl-popup-close-button').addEventListener('click', (e) => { + e.preventDefault(); + ui.hidePopup(); + }); + + // set position when content was rendered + ui.popup.setPosition(pos, ['center', 'top']); + + // display popup + popup.style.opacity = '1'; + popup.style['margin-top'] = '-1rem'; + } + + hidePopup() { + const ui = this; + const popup = ui.popup.getDiv(); + + popup.classList.add('d-none'); + if (!ui.config['noRestoreBounds'] || ui.config['flyToBounds']) { + ui.restoreBounds(); + } + + ui.el.dispatchEvent(new Event(Events.MAPPOPUPCLOSE)); + } + + geocode(addr, callback) { + const ui = this; + + ui.geocoder.geocode( + { + address: addr, + }, + (results, status) => { + if (status === 'OK') { + //results[0].geometry.location; + + if (typeof callback === 'function') { + callback(results); + } + + return results; + } else { + console.error( + `${ui.getName()}: Geocode was not successful for the following reason: ${status}`, + ); + } + }, + ); + } + + reverseGeocode(latLng, callback) { + const ui = this; + + ui.geocoder.geocode( + { + location: latlng, + }, + (results, status) => { + if (status === 'OK') { + //results[0].formatted_address; + + if (typeof callback === 'function') { + callback(results); + } + + return results; + } else { + console.error( + `${ui.getName()}: Reverse Geocoding was not successful for the following reason: ${status}`, + ); + } + }, + ); + } + + addGeoJson(config) { + const ui = this; + const geojson = JSON.parse(config['geojson']); + const firstMarker = geojson.features[0].geometry.coordinates; + //Map.setCenter(firstMarker); + const bounds = new google.maps.LatLngBounds(); + + // add markers to map + geojson.features.forEach((marker) => { + const id = marker.id; + const crds = marker.geometry.coordinates; + const content = marker.properties.content; + + ui.addMarker(crds, { + id, + content, + icon: marker.icon, + flyToMarker: config['flyToMarker'], + }); + + bounds.extend({ + lat: crds[1], + lng: crds[0], + }); + }); + + if (ui.markers.length > 1) { + ui.map.fitBounds(bounds, { + padding: 30, + }); //panToBounds + } else if (ui.markers[0]) { + ui.map.setCenter(ui.markers[0].getPosition()); + } + + ui.default_bounds = bounds; + ui.default_zoom = ui.map.getZoom(); + } + + getMap() { + const ui = this; + return ui.map; + } + + getPopup() { + const ui = this; + return ui.popup; + } + + restoreBounds() { + const ui = this; + + if (ui.default_bounds && ui.markers.length > 1) { + ui.map.fitBounds(ui.default_bounds, { + padding: 30, + }); //panToBounds + } else { + if (ui.markers[0]) { + ui.map.setCenter(ui.markers[0].getPosition()); + } + + ui.restoreZoom(); + } + } + + restoreZoom() { + const ui = this; + + ui.map.setZoom(ui.default_zoom); + } + } + + return GoogleMapsDriver; +})(window); + +export default GoogleMapsDriver; diff --git a/src/js/drivers/_map.google.marker.js b/src/js/drivers/_map.google.marker.js new file mode 100644 index 0000000..1fbfbb0 --- /dev/null +++ b/src/js/drivers/_map.google.marker.js @@ -0,0 +1,222 @@ +'use strict'; + +const Obj = { + init: () => { + class GoogleMapsHtmlOverlay extends google.maps.OverlayView { + constructor(options) { + super(); + const ui = this; + + ui.ownerMap = options.map; + //ui.setMap(options.map); + ui.position = options.position; + ui.html = options.html ? + options.html : + '
'; + ui.divClass = options.divClass; + ui.align = options.align; + ui.isDebugMode = options.debug; + ui.onClick = options.onClick; + ui.onMouseOver = options.onMouseOver; + + ui.isBoolean = (arg) => { + if (typeof arg === 'boolean') { + return true; + } else { + return false; + } + }; + + ui.isNotUndefined = (arg) => { + if (typeof arg !== 'undefined') { + return true; + } else { + return false; + } + }; + + ui.hasContent = (arg) => { + if (arg.length > 0) { + return true; + } else { + return false; + } + }; + + ui.isString = (arg) => { + if (typeof arg === 'string') { + return true; + } else { + return false; + } + }; + + ui.isFunction = (arg) => { + if (typeof arg === 'function') { + return true; + } else { + return false; + } + }; + } + onAdd() { + const ui = this; + + // Create div element. + ui.div = document.createElement('div'); + ui.div.style.position = 'absolute'; + + // Validate and set custom div class + if (ui.isNotUndefined(ui.divClass) && ui.hasContent(ui.divClass)) + ui.div.className = ui.divClass; + + // Validate and set custom HTML + if ( + ui.isNotUndefined(ui.html) && + ui.hasContent(ui.html) && + ui.isString(ui.html) + ) + ui.div.innerHTML = ui.html; + + // If debug mode is enabled custom content will be replaced with debug content + if (ui.isBoolean(ui.isDebugMode) && ui.isDebugMode) { + ui.div.className = 'debug-mode'; + ui.div.innerHTML = + '
' + + '
Debug mode
'; + ui.div.setAttribute( + 'style', + 'position: absolute;' + + 'border: 5px dashed red;' + + 'height: 150px;' + + 'width: 150px;' + + 'display: flex;' + + 'justify-content: center;' + + 'align-items: center;', + ); + } + + // Add element to clickable layer + ui.getPanes().overlayMouseTarget.appendChild(ui.div); + + // Add listeners to the element. + google.maps.event.addDomListener(ui.div, 'click', (event) => { + google.maps.event.trigger(ui, 'click'); + if (ui.isFunction(ui.onClick)) ui.onClick(); + event.stopPropagation(); + }); + + google.maps.event.addDomListener(ui.div, 'mouseover', (event) => { + google.maps.event.trigger(ui, 'mouseover'); + if (ui.isFunction(ui.onMouseOver)) ui.onMouseOver(); + event.stopPropagation(); + }); + } + + draw() { + const ui = this; + + let div = document.querySelector('.popup'); + if (!div.length) { + div = ui.div; + } + + // Calculate position of div + const projection = ui.getProjection(); + + if (!projection) { + console.log('GoogleMapsHtmlOverlay: current map is missing'); + return null; + } + + const positionInPixels = projection.fromLatLngToDivPixel(ui.getPosition()); + + // Align HTML overlay relative to original position + const offset = { + y: undefined, + x: undefined, + }; + const divWidth = div.offsetWidth; + const divHeight = div.offsetHeight; + + switch (Array.isArray(ui.align) ? ui.align.join(' ') : '') { + case 'left top': + offset.y = divHeight; + offset.x = divWidth; + break; + case 'left center': + offset.y = divHeight / 2; + offset.x = divWidth; + break; + case 'left bottom': + offset.y = 0; + offset.x = divWidth; + break; + case 'center top': + offset.y = divHeight; + offset.x = divWidth / 2; + break; + case 'center center': + offset.y = divHeight / 2; + offset.x = divWidth / 2; + break; + case 'center bottom': + offset.y = 0; + offset.x = divWidth / 2; + break; + case 'right top': + offset.y = divHeight; + offset.x = 0; + break; + case 'right center': + offset.y = divHeight / 2; + offset.x = 0; + break; + case 'right bottom': + offset.y = 0; + offset.x = 0; + break; + default: + offset.y = divHeight / 2; + offset.x = divWidth / 2; + break; + } + + // Set position + ui.div.style.top = `${positionInPixels.y - offset.y}px`; + ui.div.style.left = `${positionInPixels.x - offset.x}px`; + } + + getPosition() { + const ui = this; + return new google.maps.LatLng(ui.position); + } + + getDiv() { + const ui = this; + return ui.div; + } + + setPosition(position, align) { + const ui = this; + ui.position = position; + ui.align = align; + ui.draw(); + } + + remove() { + const ui = this; + ui.setMap(null); + ui.div.remove(); + } + + // emulate google.maps.Marker functionality for compatibility (for example with @googlemaps/markerclustererplus) + getDraggable() { + return false; + } + } + return GoogleMapsHtmlOverlay; + }, +}; + +export default Obj; diff --git a/src/js/drivers/_map.mapbox.js b/src/js/drivers/_map.mapbox.js new file mode 100644 index 0000000..b4a9fe5 --- /dev/null +++ b/src/js/drivers/_map.mapbox.js @@ -0,0 +1,187 @@ +'use strict'; + +import $ from 'jquery'; +import mapBoxGL from 'mapbox-gl'; + +import Events from '../../_events'; + +const MapBoxDriver = (($) => { + class MapBoxDriver { + getName() { + return 'MapBoxDriver'; + } + + init($el, config = []) { + const ui = this; + + mapBoxGL.accessToken = config['key']; + + ui.map = new mapBoxGL.Map({ + container: $el.find('.mapAPI-map')[0], + center: config['center'] ? config['center'] : [0, 0], + //hash: true, + style: config['style'] + ? config['style'] + : 'mapbox://styles/mapbox/streets-v9', + localIdeographFontFamily: config['font-family'], + zoom: config['mapZoom'] ? config['mapZoom'] : 10, + attributionControl: false, + antialias: true, + accessToken: config['key'], + }) + .addControl( + new mapBoxGL.AttributionControl({ + compact: true, + }), + ) + .addControl(new mapBoxGL.NavigationControl(), 'top-right') + .addControl( + new mapBoxGL.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + }, + trackUserLocation: true, + }), + 'bottom-right', + ) + .addControl( + new mapBoxGL.ScaleControl({ + maxWidth: 80, + unit: 'metric', + }), + 'top-left', + ) + .addControl(new mapBoxGL.FullscreenControl()); + + ui.map.on('load', (e) => { + $el.trigger(Events.MAPAPILOADED); + }); + + ui.popup = new mapBoxGL.Popup({ + closeOnClick: false, + className: 'popup', + }); + } + + addMarker(crds, config) { + const ui = this; + + // create a DOM el for the marker + const $el = $( + `
${config['icon']}
`, + ); + + $el.on('click', (e) => { + ui.popup + .setLngLat(crds) + .setHTML(config['content']) + .addTo(ui.map); + + if (config['flyToMarker']) { + ui.map.flyTo({ + center: crds, + zoom: 17, + }); + } + + $(e.currentTarget).trigger(Events.MAPMARKERCLICK); + }); + + // add marker to map + const marker = new mapBoxGL.Marker($el[0]).setLngLat(crds).addTo(ui.map); + + return marker; + } + + addGeoJson(config) { + const ui = this; + // Insert the layer beneath any symbol layer. + /*if (config['3d']) { + const layers = Map.getStyle().layers; + let labelLayerId; + for (let i = 0; i < layers.length; i++) { + if (layers[i].type === 'symbol' && layers[i].layout['text-field']) { + labelLayerId = layers[i].id; + break; + } + } + + Map.addLayer({ + 'id': '3d-buildings', + 'source': 'composite', + 'source-layer': 'building', + 'filter': ['==', 'extrude', 'true'], + 'type': 'fill-extrusion',flyToBounds + 'minzoom': 15, + 'paint': { + 'fill-extrusion-color': '#aaa', + + // use an 'interpolate' expression to add a smooth transition effect to the + // buildings as the user zooms in + 'fill-extrusion-height': [ + "interpolate", ["linear"], + ["zoom"], + 15, 0, + 15.05, ["get", "height"], + ], + 'fill-extrusion-base': [ + "interpolate", ["linear"], + ["zoom"], + 15, 0, + 15.05, ["get", "min_height"], + ], + 'fill-extrusion-opacity': .6, + }, + }, labelLayerId); + }*/ + + const firstMarker = config['geojson'].features[0].geometry.coordinates; + //Map.setCenter(firstMarker); + const bounds = new mapBoxGL.LngLatBounds(firstMarker, firstMarker); + + // add markers to map + config['geojson'].features.forEach((marker) => { + const id = marker.id; + const crds = marker.geometry.coordinates; + const content = marker.properties.content; + + ui.addMarker(crds, { + id, + content, + icon: marker.icon, + flyToMarker: config['flyToMarker'], + }); + + bounds.extend(crds); + }); + + ui.map.fitBounds(bounds, { + padding: 30, + }); + + ui.popup.on('close', (e) => { + if (config['flyToBounds']) { + ui.map.fitBounds(bounds, { + padding: 30, + }); + } + + $(e.currentTarget).trigger(Events.MAPPOPUPCLOSE); + }); + } + + getMap() { + const ui = this; + return ui.map; + } + + getPopup() { + const ui = this; + return ui.popup; + } + } + + return MapBoxDriver; +})($); + +export default MapBoxDriver; diff --git a/src/scss/_ui/map.api.scss b/src/scss/_ui/map.api.scss new file mode 100644 index 0000000..72bacf7 --- /dev/null +++ b/src/scss/_ui/map.api.scss @@ -0,0 +1,134 @@ +@import '../_variables'; +@import '../_animations'; + +//@import "~mapbox-gl/src/css/mapbox-gl.css"; +$map-height: 30rem !default; + +$map-marker-color: $primary !default; +$map-marker-size: 30px !default; + +$map-popup-font-size: 0.8rem !default; +$map-popup-width: 16rem !default; +$map-popup-height: 7rem !default; +$map-popup-bg: $white !default; +$map-popup-color: $body-color !default; + +.mapAPI-map { + height: $map-height; + //margin-bottom: $grid-gutter-element-height; +} + +.mapboxgl { + &-popup { + width: $map-popup-width; + height: $map-popup-height; + font-size: $map-popup-font-size; + line-height: 1.2em; + position: absolute; + top: 0; + left: 0; + display: flex; + pointer-events: none; + z-index: 4; + } + + &-popup-anchor-bottom, + &-popup-anchor-bottom-left, + &-popup-anchor-bottom-right { + flex-direction: column-reverse; + } + + &-popup-content { + min-width: $map-popup-width; + background: $map-popup-bg; + color: $map-popup-color; + position: relative; + pointer-events: auto; + padding: 0.8rem; + border-radius: 0.25rem; + min-height: 5rem; + box-shadow: 0 0.1rem 0.8rem 0 rgba(0, 0, 0, 0.4); + } + + &-popup-close-button { + position: absolute; + right: 0; + top: 0; + font-size: 2rem; + padding: 0.5rem; + border-top-right-radius: 0.25rem; + z-index: 2; + + &:hover, + &:focus { + background: $primary; + color: $white; + } + } + + &-popup-tip { + width: 0; + height: 0; + border: 0.8rem solid transparent; + z-index: 1; + } + + &-popup-anchor-bottom &-popup-tip { + border-top-color: $map-popup-bg; + align-self: center; + border-bottom: none; + } + + &-marker { + width: $map-marker-size; + height: $map-marker-size; + font-size: $map-marker-size; + line-height: 1em; + color: $map-marker-color; + cursor: pointer; + text-align: center; + display: flex; + align-items: flex-end; + justify-content: center; + + .marker-icon, + .fas, + .fab, + .far { + animation: pulse 0.8s linear infinite; + } + } + + &-cluster { + background: $info; + color: color-yiq($info); + border-radius: 100%; + font-weight: bold; + font-size: 1.2rem; + display: flex; + align-items: center; + animation: pulse 0.8s linear infinite; + + &::before, + &::after { + content: ""; + display: block; + position: absolute; + width: 140%; + height: 140%; + + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + background: $info; + opacity: 0.2; + border-radius: 100%; + z-index: -1; + } + + &::after { + width: 180%; + height: 180%; + } + } +}