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: ``,
+ 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%;
+ }
+ }
+}