IMPR: reorganize files, minor updates

This commit is contained in:
Tony Air 2021-08-09 18:04:09 +02:00
parent f3590d2f63
commit fec5e2e2f8
57 changed files with 4108 additions and 0 deletions

View File

@ -0,0 +1,26 @@
import {
InMemoryCache,
} from '@apollo/client';
//import { IonicStorageModule } from '@ionic/storage';
//import { persistCache, IonicStorageWrapper } from 'apollo3-cache-persist';
import {
persistCacheSync,
LocalStorageWrapper,
} from 'apollo3-cache-persist';
const cache = new InMemoryCache();
// await before instantiating ApolloClient, else queries might run before the cache is persisted
//await persistCache({
persistCacheSync({
cache,
storage: new LocalStorageWrapper(window.localStorage),
key: 'web-persist',
maxSize: 1048576, // 1Mb
//new IonicStorageWrapper(IonicStorageModule),
});
export {
cache,
};

122
src/js/ajax/apollo/init.js Normal file
View File

@ -0,0 +1,122 @@
import Events from '../../_events';
import {
cache,
} from './cache';
import {
from,
ApolloClient,
HttpLink,
ApolloLink,
concat,
} from '@apollo/client';
import {
onError,
} from '@apollo/client/link/error';
const NAME = 'appolo';
const API_META = document.querySelector('meta[name="api_url"]');
const API_URL = API_META ?
API_META.getAttribute('content') :
`${window.location.protocol }//${ window.location.host }/graphql`;
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
operation.setContext({
headers: {
apikey: `${GRAPHQL_API_KEY}`,
},
});
return forward(operation);
});
console.info(`%cAPI: ${API_URL}`, 'color:green;font-size:10px');
const link = from([
authMiddleware,
new ApolloLink((operation, forward) => {
operation.setContext({
start: new Date(),
});
return forward(operation);
}),
onError(({
operation,
response,
graphQLErrors,
networkError,
forward,
}) => {
if (operation.operationName === 'IgnoreErrorsQuery') {
console.error(`${NAME}: IgnoreErrorsQuery`);
response.errors = null;
return;
}
if (graphQLErrors) {
graphQLErrors.forEach(({
message,
locations,
path,
}) =>
console.error(
`${NAME}: [GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
}
if (networkError) {
/*let msg = '';
switch (networkError.statusCode) {
case 404:
msg = 'Not Found.';
break;
case 500:
msg = 'Server issue, please try again latter.';
break;
default:
msg = 'Something went wrong.';
break;
}*/
console.error(`${NAME}: [Network error] ${networkError.statusCode}`);
}
console.error(`${NAME}: [APOLLO_ERROR]`);
window.dispatchEvent(new Event(Events.APOLLO_ERROR));
}),
new ApolloLink((operation, forward) => {
return forward(operation).map((data) => {
// data from a previous link
const time = new Date() - operation.getContext().start;
console.log(
`${NAME}: operation ${operation.operationName} took ${time} ms to complete`,
);
window.dispatchEvent(new Event(Events.ONLINE));
return data;
});
}),
new HttpLink({
uri: API_URL,
// Use explicit `window.fetch` so tha outgoing requests
// are captured and deferred until the Service Worker is ready.
fetch: (...args) => fetch(...args),
credentials: 'same-origin', //'include',
connectToDevTools: process.env.NODE_ENV === 'development' ? true : false,
}),
]);
// Isolate Apollo client so it could be reused
// in both application runtime and tests.
const client = new ApolloClient({
cache,
link,
});
export {
client,
};

View File

@ -0,0 +1,79 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
const axios = require('axios');
export default ((W) => {
const NAME = 'main.lazy-images';
const D = document;
const BODY = D.body;
const API_STATIC = document.querySelector('meta[name="api_static_domain"]');
const API_STATIC_URL = API_STATIC ?
API_STATIC.getAttribute('content') :
`${window.location.protocol}//${window.location.host}`;
console.log(`${NAME} [static url]: ${API_STATIC_URL}`);
const loadLazyImages = () => {
console.log(`${NAME}: Load lazy images`);
D.querySelectorAll(`[data-lazy-src]`).forEach((el) => {
el.classList.remove('empty');
el.classList.add('loading');
el.classList.remove('loading__network-error');
const attr = el.getAttribute('data-lazy-src');
const imageUrl = attr.startsWith('http') ? attr : API_STATIC_URL + attr;
// offline response will be served by caching service worker
axios
.get(imageUrl, {
responseType: 'blob',
})
.then((response) => {
const reader = new FileReader(); // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/FileReader
reader.readAsDataURL(response.data);
reader.onload = () => {
const imageDataUrl = reader.result;
el.setAttribute('src', imageDataUrl);
el.classList.remove('loading');
el.classList.add('loading__success');
};
})
.catch((e) => {
//el.setAttribute('src', imageUrl);
if (e.response) {
switch (e.response.status) {
case 404:
msg = 'Not Found.';
break;
case 500:
msg = 'Server issue, please try again latter.';
break;
default:
msg = 'Something went wrong.';
break;
}
console.error(`${NAME} [${imageUrl}]: ${msg}`);
} else if (e.request) {
msg = 'No response received';
console.error(`${NAME} [${imageUrl}]: ${msg}`);
} else {
console.error(`${NAME} [${imageUrl}]: ${e.message}`);
}
el.classList.remove('loading');
el.classList.add('loading__network-error');
el.classList.add('empty');
});
});
};
W.addEventListener(`${Events.LODEDANDREADY}`, loadLazyImages);
W.addEventListener(`${Events.AJAX}`, loadLazyImages);
})(window);

227
src/js/ajax/links.js Normal file
View File

@ -0,0 +1,227 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
import Page from './models/page.jsx';
import {
getParents,
} from '../main/funcs';
import {
Collapse,
} from 'bootstrap';
import SpinnerUI from '../main/loading-spinner';
const MainUILinks = ((W) => {
const NAME = 'main.links';
const D = document;
const BODY = D.body;
class MainUILinks {
window
static init() {
const ui = this;
ui.GraphPage = null;
console.log(`${NAME}: init`);
ui.loaded();
// history state switch
W.addEventListener('popstate', (e) => {
ui.popState(e);
});
}
static loaded() {
const ui = this;
D.querySelectorAll('.graphql-page').forEach((el, i) => {
const el_id = el.getAttribute('href');
el.setAttribute(`data-${ui.name}-id`, el_id);
el.addEventListener('click', ui.loadClick);
});
}
static setActiveLinks(link) {
const ui = this;
D.querySelectorAll(`[data-${ui.name}-id="${link}"]`).forEach(
(el) => {
el.classList.add('active');
},
);
}
static reset() {
// reset focus
D.activeElement.blur();
// remove active and loading classes
D.querySelectorAll('.graphql-page,.nav-item').forEach((el2) => {
el2.classList.remove('active', 'loading');
});
}
static popState(e) {
const ui = this;
SpinnerUI.show();
if (e.state && e.state.page) {
console.log(`${NAME}: [popstate] load`);
const state = JSON.parse(e.state.page);
state.current = null;
state.popstate = true;
ui.reset();
ui.setActiveLinks(e.state.link);
if (!ui.GraphPage) {
console.log(
`${NAME}: [popstate] GraphPage is missing. Have to render it first`,
);
ui.GraphPage = ReactDOM.render(
<Page />,
document.getElementById('MainContent'),
);
}
ui.GraphPage.setState(state);
SpinnerUI.hide();
window.dispatchEvent(new Event(Events.AJAX));
} else if (e.state && e.state.landing) {
console.log(`${NAME}: [popstate] go to landing`);
W.location.href = e.state.landing;
} else {
console.warn(`${NAME}: [popstate] state is missing`);
console.log(e);
SpinnerUI.hide();
}
}
// link specific event {this} = current event, not MainUILinks
static loadClick(e) {
console.groupCollapsed(`${NAME}: load on click`);
e.preventDefault();
const ui = MainUILinks;
const el = e.currentTarget;
SpinnerUI.show();
ui.reset();
el.classList.add('loading');
el.classList.remove('response-404', 'response-500', 'response-523');
BODY.classList.add('ajax-loading');
// hide parent mobile nav
const navs = getParents(el, '.collapse');
if (navs.length) {
navs.forEach((nav) => {
const collapseInst = Collapse.getInstance(nav);
if (collapseInst) {
collapseInst.hide();
}
});
}
// hide parent dropdown
/*const dropdowns = getParents(el, '.dropdown-menu');
if (dropdowns.length) {
const DropdownInst = Dropdown.getInstance(dropdowns[0]);
DropdownInst.hide();
}*/
if (!ui.GraphPage) {
ui.GraphPage = ReactDOM.render(
<Page />,
document.getElementById('MainContent'),
);
}
const link = el.getAttribute('href') || el.getAttribute('data-href');
ui.GraphPage.state.current = el;
ui.GraphPage.load(link)
.then((response) => {
BODY.classList.remove('ajax-loading');
el.classList.remove('loading');
el.classList.add('active');
D.loading_apollo_link = null;
if (ui.GraphPage.state.Link) {
window.history.pushState({
page: JSON.stringify(ui.GraphPage.state),
link: el.getAttribute(`data-${ui.name}-id`),
},
ui.GraphPage.state.Title,
ui.GraphPage.state.Link,
);
ui.setActiveLinks(ui.GraphPage.state.Link)
}
SpinnerUI.hide();
window.dispatchEvent(new Event(Events.AJAX));
console.groupEnd(`${NAME}: load on click`);
})
.catch((e) => {
console.error(`${NAME}: loading error`);
console.log(e);
/*BODY.classList.remove('ajax-loading');
el.classList.remove('loading');*/
el.classList.add('error', `response-${e.status}`);
/*switch (e.status) {
case 500:
break;
case 404:
el.classList.add('not-found');
break;
case 523:
el.classList.add('unreachable');
break;
}*/
//SpinnerUI.hide();
//window.dispatchEvent(new Event(Events.AJAX));
console.groupEnd(`${NAME}: load on click`);
console.log(`${NAME}: reloading page ${link}`);
// fallback loading
W.location.href = link;
});
}
}
W.addEventListener(`${Events.LOADED}`, () => {
MainUILinks.init();
});
W.addEventListener(`${Events.AJAX}`, () => {
MainUILinks.loaded();
});
// fallback
/*W.addEventListener(`${Events.APOLLO_ERROR}`, (e) => {
console.error(`${NAME}: [APOLLO_ERROR] loading failure, reloading the page`);
//W.dispatchEvent(new Event(Events.OFFLINE));
if (D.loading_apollo_link) {
W.location.href = D.loading_apollo_link;
}
});*/
})(window);
export default MainUILinks;

View File

@ -0,0 +1,229 @@
/*
* Lightbox window
*/
import {
Component
} from 'react';
import Events from '../../events';
import {
client
} from '../apollo/init';
import {
gql
} from '@apollo/client';
class Page extends Component {
state = {
type: [],
shown: false,
loading: false,
error: false,
current: null,
ID: null,
URLSegment: null,
ClassName: 'Page',
CSSClass: null,
Title: null,
Summary: null,
Link: null,
URL: null,
Elements: [],
page: null,
};
componentDidUpdate() {
const ui = this;
if (ui.state.Title) {
document.title = ui.state.Title;
if (ui.state.URL) {
window.history.pushState(
{
page: JSON.stringify(ui.state)
},
ui.state.Title,
ui.state.URL,
);
}
}
if (ui.state.Elements.length) {
window.dispatchEvent(new Event(Events.AJAX));
}
}
constructor(props) {
super(props);
const ui = this;
ui.name = ui.constructor.name;
console.log(`${ui.name}: init`);
}
reset = () => {
const ui = this;
ui.setState({
type: [],
shown: false,
loading: false,
error: false,
ID: null,
Title: null,
URL: null,
Elements: [],
});
};
load = (link) => {
const ui = this;
const query = gql(`
query Pages {
readPages(URLSegment: "home", limit: 1, offset: 0) {
edges {
node {
__typename
_id
ID
Title
ClassName
CSSClass
Summary
Link
URLSegment
Elements {
edges {
node {
__typename
_id
ID
Title
Render
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
`);
ui.reset();
ui.setState({
Title: 'Loading ...',
loading: true,
});
console.log(client.readQuery({
query
}));
client
.query({
query: query,
})
.then((resp) => {
const page = resp.data.readPages.edges[0].node;
// write to cache
client.writeQuery({
query,
data: {
resp
}
});
console.log(client.readQuery({
query
}));
ui.setState({
ID: page.ID,
Title: page.Title,
ClassName: page.ClassName,
URLSegment: page.URLSegment,
CSSClass: page.CSSClass,
Summary: page.Summary,
Link: page.Link,
Elements: page.Elements.edges,
URL: page.Link || link,
loading: false,
});
})
.catch((error) => {
console.error(error);
let msg = '';
if (error.response) {
switch (error.response.status) {
case 404:
msg = 'Not Found.';
break;
case 500:
msg = 'Server issue, please try again latter.';
break;
default:
msg = 'Something went wrong.';
break;
}
} else if (error.request) {
msg = 'No response received';
} else {
console.warn('Error', error.message);
}
ui.setState({
error: msg
});
});
};
getHtml = (html) => {
const decodeHtmlEntity = (input) => {
var doc = new DOMParser().parseFromString(input, 'text/html');
return doc.documentElement.textContent;
};
return {
__html: decodeHtmlEntity(html)
};
};
render() {
const ui = this;
const name = ui.name;
const className = `elemental-area page-${ui.state.CSSClass} url-${ui.state.URLSegment}`;
const ElementItem = (props) => (
<div dangerouslySetInnerHTML={props.html}></div>
);
let html = '';
if (ui.state.Elements.length) {
ui.state.Elements.map((el) => {
html += el.node.Render;
});
} else {
html += '<div class="loading">Loading ...</div>';
}
return (
<div
className={className}
dangerouslySetInnerHTML={ui.getHtml(html)}
></div>
);
}
}
export default Page;

233
src/js/ajax/models/page.jsx Normal file
View File

@ -0,0 +1,233 @@
/*
* page #MainContent area
*/
import {
Component
} from 'react';
import {
useQuery,
gql
} from '@apollo/client';
import {
client
} from '../apollo/init';
import {
cache
} from '../apollo/cache';
const D = document;
const BODY = document.body;
class Page extends Component {
state = {
type: [],
shown: false,
Title: 'Loading ...',
loading: true,
error: false,
current: null,
ID: null,
URLSegment: null,
ClassName: 'Page',
CSSClass: null,
Summary: null,
Link: null,
URL: null,
HTML: null,
Elements: [],
page: null,
};
componentDidUpdate() {
const ui = this;
if (ui.state.Title) {
document.title = ui.state.Title;
}
}
constructor(props) {
super(props);
const ui = this;
ui.name = ui.constructor.name;
ui.empty_state = ui.state;
console.log(`${ui.name}: init`);
}
isOnline = () => {
return BODY.classList.contains('is-online');
};
load = (link) => {
const ui = this;
return new Promise((resolve, reject) => {
const query = gql(`query Pages {
readPages(Link: "${link}") {
edges {
node {
__typename
_id
ID
Title
ClassName
CSSClass
Summary
Link
URLSegment
HTML
}
}
}
}`);
if (!ui.isOnline()) {
const data = client.readQuery({
query
});
if (data && ui.processResponse(data)) {
console.log(`${ui.name}: Offline cached response`);
resolve(data);
} else {
console.log(`${ui.name}: No offline response`);
ui.setState(
Object.assign(ui.empty_state, {
Title: 'Offline',
CSSClass: 'graphql__status-523',
Summary: "You're Offline. The page is not available now.",
loading: false,
}),
);
reject({
status: 523
});
}
} else {
if (!ui.state.loading) {
ui.setState(ui.empty_state);
}
client
.query({
query: query,
fetchPolicy: ui.isOnline() ? 'no-cache' : 'cache-first',
})
.then((resp) => {
// write to cache
const data = resp.data;
client.writeQuery({
query,
data: data
});
if (ui.processResponse(data)) {
console.log(`${ui.name}: got the server response`);
resolve(data);
} else {
console.log(`${ui.name}: not found`);
reject({
status: 404
});
}
})
.catch((error) => {
reject({
status: 500,
error: error
});
});
}
});
};
processResponse = (data) => {
const ui = this;
if (!data.readPages.edges.length) {
console.log(`${ui.name}: not found`);
ui.setState(
Object.assign(ui.empty_state, {
Title: 'Not Found',
CSSClass: 'graphql__status-404',
Summary: 'The page is not found.',
loading: false,
}),
);
return false;
}
const page = data.readPages.edges[0].node;
ui.setState({
ID: page.ID,
Title: page.Title,
ClassName: page.ClassName,
URLSegment: page.URLSegment,
CSSClass: page.CSSClass,
Summary: page.Summary,
Link: page.Link,
HTML: page.HTML,
Elements: [], //page.Elements.edges,
loading: false,
});
return true;
};
getHtml = (html) => {
const decodeHtmlEntity = (input) => {
var doc = new DOMParser().parseFromString(input, 'text/html');
return doc.documentElement.textContent;
};
return {
__html: decodeHtmlEntity(html)
};
};
render() {
const ui = this;
const name = ui.name;
const className = `elemental-area graphql__page page-${ui.state.CSSClass} url-${ui.state.URLSegment}`;
const ElementItem = (props) => (
<div dangerouslySetInnerHTML={props.html}></div>
);
let html = '';
if (ui.state.HTML) {
console.log(`${ui.name}: HTML only`);
html = ui.state.HTML;
} else if (ui.state.Elements.length) {
console.log(`${ui.name}: render`);
ui.state.Elements.map((el) => {
html += el.node.Render;
});
} else if (ui.state.Summary && ui.state.Summary.length) {
console.log(`${ui.name}: summary only`);
html = `<div class="summary">${ui.state.Summary}</div>`;
}
if (ui.state.loading) {
const spinner = D.getElementById('PageLoading');
html = `<div class="loading">Loading ...</div>`;
}
return (
<div
className={className}
dangerouslySetInnerHTML={ui.getHtml(html)}
></div>
);
}
}
export default Page;

104
src/js/ajax/online.js Normal file
View File

@ -0,0 +1,104 @@
// ping online/offline state switch and detection
import Events from '../_events';
import Consts from '../_consts';
const axios = require('axios');
export default ((W) => {
const NAME = 'main.online';
const D = document;
const BODY = D.body;
let pingInterval;
const PING_META = D.querySelector('meta[name="ping"]');
let update_online_status_lock = false;
const UPDATE_ONLINE_STATUS = (online) => {
if (update_online_status_lock) {
return;
}
update_online_status_lock = true;
if (online) {
if (BODY.classList.contains('is-offline')) {
console.log(`${NAME}: back Online`);
W.dispatchEvent(new Event(Events.BACKONLINE));
} else {
console.log(`${NAME}: Online`);
W.dispatchEvent(new Event(Events.ONLINE));
}
BODY.classList.add('is-online');
BODY.classList.remove('is-offline');
if (PING_META && !pingInterval) {
console.log(`${NAME}: SESSION_PING is active`);
pingInterval = setInterval(SESSION_PING, 300000); // 5 min in ms
}
} else {
console.log(`${NAME}: Offline`);
BODY.classList.add('is-offline');
BODY.classList.remove('is-online');
clearInterval(pingInterval);
pingInterval = null;
W.dispatchEvent(new Event(Events.OFFLINE));
}
update_online_status_lock = false;
};
// session ping
let session_ping_lock = false;
const SESSION_PING = () => {
if (session_ping_lock || BODY.classList.contains('is-offline')) {
return;
}
const PING_URL = PING_META.getAttribute('content');
console.log(`${NAME}: session ping`);
session_ping_lock = true;
axios
.post(PING_URL, {})
.then((resp) => {
session_ping_lock = false;
UPDATE_ONLINE_STATUS(true);
})
.catch((error) => {
console.error(error);
console.warn(`${NAME}: SESSION_PING failed`);
session_ping_lock = false;
UPDATE_ONLINE_STATUS(false);
});
};
// current browser online state
const navigatorStateUpdate = () => {
if (typeof navigator.onLine !== 'undefined') {
if (!navigator.onLine) {
UPDATE_ONLINE_STATUS(false);
} else {
UPDATE_ONLINE_STATUS(true);
}
}
};
W.addEventListener(`${Events.OFFLINE}`, () => {
UPDATE_ONLINE_STATUS(false);
});
W.addEventListener(`${Events.ONLINE}`, () => {
UPDATE_ONLINE_STATUS(true);
});
W.addEventListener(`${Events.LOADED}`, navigatorStateUpdate);
W.addEventListener(`${Events.AJAX}`, navigatorStateUpdate);
})(window);

View File

@ -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);
});

View File

@ -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 :
'<div class="mapboxgl-marker"><i class="marker-icon fas fa-map-marker-alt"></i></div>'
);
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 =
'<div style="height: 10px; width: 10px; background: red; border-radius: 100%;"></div>' +
'<div style="position: absolute; top: 5px; padding: 5px; width: 130px; text-align: center; font-size: 18px; text-transform: uppercase; font-weight: bolder; background: red; color: white; font-family: Arial;">Debug mode</div>';
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;

View File

@ -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: '<div class="mapboxgl-popup-tip"></div><div class="mapboxgl-popup-content">' +
'<div class="mapboxgl-popup-close-button" type="button" aria-label="Close popup">×</div>' +
'<div class="html"></div>' +
'</div>',
});
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: `<div class="mapboxgl-marker"><div id="Marker${config['id']}" data-id="${config['id']}" class="marker">${config['icon']}</div></div>`,
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;

View File

@ -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 :
'<div class="mapboxgl-marker"><i class="marker-icon fas fa-map-marker-alt"></i></div>';
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 =
'<div style="height: 10px; width: 10px; background: red; border-radius: 100%;"></div>' +
'<div style="position: absolute; top: 5px; padding: 5px; width: 130px; text-align: center; font-size: 18px; text-transform: uppercase; font-weight: bolder; background: red; color: white; font-family: Arial;">Debug mode</div>';
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;

View File

@ -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 = $(
`<div id="Marker${config['id']}" data-id="${config['id']}" class="marker">${config['icon']}</div>`,
);
$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;

50
src/js/layout/index.js Normal file
View File

@ -0,0 +1,50 @@
import Events from '../_events';
const LayoutUI = ((W) => {
const NAME = '_layout';
const D = document;
const BODY = D.body;
const init_fonts = () => {
console.log(`${NAME}: init_fonts`);
const css = D.createElement('link');
css.rel = 'stylesheet';
css.type = 'text/css';
css.media = 'all';
css.href = 'https://fonts.googleapis.com/css?family=Roboto:ital,wght@0,400;0,700;1,400&display=swap';
D.getElementsByTagName('head')[0].appendChild(css);
};
const init_analytics = () => {
console.log(`${NAME}: init_analytics`);
/*google analytics */
/*(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
(i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
}),
(i[r].l = 1 * new Date());
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(
window,
document,
'script',
'//www.google-analytics.com/analytics.js',
'ga',
);
ga('create', 'UA-********-*', 'auto');
ga('send', 'pageview');*/
}
W.addEventListener(`${Events.LOADED}`, () => {
init_fonts();
//init_analytics();
});
})(window);
export default LayoutUI;

9
src/js/libs/log.js Normal file
View File

@ -0,0 +1,9 @@
var debug = process.env.NODE_ENV === 'development' ? true : false;
const log = (msg) => {
if (debug) {
console.log(msg);
}
};
module.exports = log;

View File

@ -0,0 +1,54 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.css-screen-size';
const D = document;
const BODY = D.body;
const detectCSSScreenSize = () => {
const el = D.createElement('div');
el.className = 'env-test';
BODY.appendChild(el);
const envs = [...Consts.ENVS].reverse();
let curEnv = envs.shift();
BODY.classList.remove(...envs);
for (let i = 0; i < envs.length; ++i) {
const env = envs[i];
el.classList.add(`d-${env}-none`);
if (W.getComputedStyle(el).display === 'none') {
curEnv = env;
BODY.classList.add(`${curEnv}`);
break;
}
}
let landscape = true;
if (W.innerWidth > W.innerHeight) {
BODY.classList.add('landscape');
BODY.classList.remove('portrait');
} else {
landscape = false;
BODY.classList.add('portrait');
BODY.classList.remove('landscape');
}
console.log(
`${NAME}: screen size detected ${curEnv} | landscape ${landscape}`,
);
BODY.removeChild(el);
return curEnv;
};
W.addEventListener(`${Events.LOADED}`, detectCSSScreenSize);
W.addEventListener(`${Events.RESIZE}`, detectCSSScreenSize);
})(window);

35
src/js/main/funcs.js Normal file
View File

@ -0,0 +1,35 @@
const funcs = {};
/*!
* Get all of an element's parent elements up the DOM tree
* (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {Node} elem The element
* @param {String} selector Selector to match against [optional]
* @return {Array} The parent elements
*/
funcs.getParents = (elem, selector) => {
// Setup parents array
const parents = [];
let el = elem;
// Get matching parent elements
while (el && el !== document) {
// If using a selector, add matching parents to array
// Otherwise, add all parents
if (selector) {
if (el.matches(selector)) {
parents.push(el);
}
} else {
parents.push(el);
}
// Jump to the next parent node
el = el.parentNode;
}
return parents;
};
module.exports = funcs;
module.exports.default = funcs;

7
src/js/main/index.js Normal file
View File

@ -0,0 +1,7 @@
import Events from '../_events';
import Consts from '../_consts';
import './visibility';
import './touch';
import './css-screen-size';
import './main';

View File

@ -0,0 +1,21 @@
// browser tab visibility state detection
import Events from '../_events';
const NAME = '_main.loading-spinner';
const D = document;
const BODY = D.body;
const SPINNER = D.getElementById('PageLoading');
class SpinnerUI {
static show() {
console.log(`${NAME}: show`);
SPINNER.classList.remove('d-none');
}
static hide() {
console.log(`${NAME}: hide`);
SPINNER.classList.add('d-none');
}
}
export default SpinnerUI;

85
src/js/main/main.js Normal file
View File

@ -0,0 +1,85 @@
import Events from '../_events';
import Consts from '../_consts';
import SpinnerUI from './loading-spinner';
const MainUI = ((W) => {
const NAME = '_main';
const D = document;
const BODY = D.body;
console.info(
`%cUI Kit ${UINAME} ${UIVERSION}`,
'color:yellow;font-size:14px',
);
console.info(
`%c${UIMetaNAME} ${UIMetaVersion}`,
'color:yellow;font-size:12px',
);
console.info(
`%chttps://github.com/a2nt/webpack-bootstrap-ui-kit by ${UIAUTHOR}`,
'color:yellow;font-size:10px',
);
console.info(`%cENV: ${process.env.NODE_ENV}`, 'color:green;font-size:10px');
console.groupCollapsed('Events');
Object.keys(Events).forEach((k) => {
console.info(`${k}: ${Events[k]}`);
});
console.groupEnd('Events');
console.groupCollapsed('Consts');
Object.keys(Consts).forEach((k) => {
console.info(`${k}: ${Consts[k]}`);
});
console.groupEnd('Events');
console.groupCollapsed('Init');
console.time('init');
class MainUI {
// first time the website initialization
static init() {
const ui = this;
// store landing page state
W.history.replaceState(
{
landing: W.location.href,
},
D.title,
W.location.href,
);
//
ui.loaded();
}
// init AJAX components
static loaded() {
const ui = this;
console.log(`${NAME}: loaded`);
}
}
W.addEventListener(`${Events.LOADED}`, () => {
MainUI.init();
BODY.classList.add('loaded');
SpinnerUI.hide();
console.groupEnd('init');
console.timeEnd('init');
W.dispatchEvent(new Event(Events.LODEDANDREADY));
});
W.addEventListener(`${Events.AJAX}`, () => {
MainUI.loaded();
});
W.MainUI = MainUI;
return MainUI;
})(window);
export default MainUI;

70
src/js/main/touch.js Normal file
View File

@ -0,0 +1,70 @@
// touch/mouse detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.touch';
const D = document;
const BODY = D.body;
let prev_touch_event_name;
let touch_timeout;
const SET_TOUCH_SCREEN = (bool, event_name) => {
if (touch_timeout || event_name === prev_touch_event_name) {
return;
}
if (bool) {
console.log(`${NAME}: Touch screen enabled`);
BODY.classList.add('is-touch');
BODY.classList.remove('is-mouse');
W.dispatchEvent(new Event(Events.TOUCHENABLE));
} else {
console.log(`${NAME}: Touch screen disabled`);
BODY.classList.add('is-mouse');
BODY.classList.remove('is-touch');
W.dispatchEvent(new Event(Events.TOUCHDISABLED));
}
prev_touch_event_name = event_name;
// prevent firing touch and mouse events together
if (!touch_timeout) {
touch_timeout = setTimeout(() => {
clearTimeout(touch_timeout);
touch_timeout = null;
}, 500);
}
};
SET_TOUCH_SCREEN(
'ontouchstart' in W ||
navigator.MaxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0 ||
W.matchMedia('(hover: none)').matches,
'init',
);
D.addEventListener('touchend', (e) => {
let touch = false;
if (e.type !== 'click') {
touch = true;
}
SET_TOUCH_SCREEN(touch, 'click-touchend');
});
// disable touch on mouse events
D.addEventListener('click', (e) => {
let touch = false;
if (e.type !== 'click') {
touch = true;
}
SET_TOUCH_SCREEN(touch, 'click-touchend');
});
})(window);

34
src/js/main/visibility.js Normal file
View File

@ -0,0 +1,34 @@
// browser tab visibility state detection
import Events from '../_events';
import Consts from '../_consts';
export default ((W) => {
const NAME = '_main.visibility';
const D = document;
const BODY = D.body;
// update visibility state
// get browser window visibility preferences
// Opera 12.10, Firefox >=18, Chrome >=31, IE11
const HiddenName = 'hidden';
const VisibilityChangeEvent = 'visibilitychange';
D.addEventListener(VisibilityChangeEvent, () => {
if (D.visibilityState === HiddenName) {
console.log(`${NAME}: Tab: hidden`);
BODY.classList.add('is-hidden');
BODY.classList.remove('is-focused');
W.dispatchEvent(new Event(Events.TABHIDDEN));
} else {
console.log(`${NAME}: Tab: focused`);
BODY.classList.add('is-focused');
BODY.classList.remove('is-hidden');
W.dispatchEvent(new Event(Events.TABFOCUSED));
}
});
})(window);

View File

@ -0,0 +1,8 @@
import {
createStore,
} from 'redux'
import reducers from '../reducers'
export default function configureStore() {
return createStore(reducers)
}

8
src/js/store/connect.js Normal file
View File

@ -0,0 +1,8 @@
import {
createStore,
} from 'redux'
import reducers from '../reducers'
export default function configureStore() {
return createStore(reducers)
}

86
src/js/ui/carousel.js Normal file
View File

@ -0,0 +1,86 @@
import Events from '../_events';
import Carousel from 'bootstrap/js/src/carousel';
const CarouselUI = ((window) => {
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('aria-label', 'Next Slide');
next.setAttribute('data-bs-target', el.getAttribute('id'));
next.setAttribute('data-bs-slide', 'next');
next.addEventListener('click', (e) => {
carousel.next();
});
next.innerHTML = '<span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next</span>';
el.appendChild(next);
const prev = document.createElement('button');
prev.setAttribute('type', 'button');
prev.setAttribute('aria-label', 'Previous Slide');
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 = '<span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous</span>';
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');
ind.setAttribute('aria-label', `Slide to #${ i + 1}`);
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);
export default CarouselUI;

View File

@ -0,0 +1,95 @@
// api-less instagram feed
// Visitor network maybe temporary banned by Instagram because of too many requests from external websites
// so it isn't very stable implementation. You should have something for the fall-back.
import Events from '../_events';
import Consts from '../_consts';
import InstagramFeed from '@jsanahuja/instagramfeed/src/InstagramFeed';
export default ((window) => {
const NAME = 'js-instagramfeed';
const BODY = document.body;
const ig_media_preview = (base64data) => {
const jpegtpl =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABsaGikdKUEmJkFCLy8vQkc/Pj4/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0cBHSkpNCY0PygoP0c/NT9HR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR//AABEIABQAKgMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AA==',
t = atob(base64data),
p = t.slice(3).split(''),
o = t
.substring(0, 3)
.split('')
.map((e) => {
return e.charCodeAt(0);
}),
c = atob(jpegtpl).split('');
c[162] = String.fromCharCode(o[1]);
c[160] = String.fromCharCode(o[2]);
return base64data ?
`data:image/jpeg;base64,${btoa(c.concat(p).join(''))}` :
null;
};
const loadFeed = () => {
console.log(`${NAME}: loading`);
document.querySelectorAll(`.${NAME}`).forEach((el, i) => {
const ID = `InstagramFeed${i}`;
const dataset = el.dataset;
el.classList.add(`${NAME}-loading`);
el.classList.remove(`${NAME}-loaded`, `${NAME}-error`);
new InstagramFeed({
username: dataset['username'],
tag: dataset['tag'] || null,
display_profile: dataset['display-profile'],
display_biography: dataset['display-biography'],
display_gallery: dataset['display-gallery'],
display_captions: dataset['display-captions'],
cache_time: dataset['cache_time'] || 360,
items: dataset['items'] || 12,
styling: false,
lazy_load: true,
callback: (data) => {
console.log(`${NAME}: data response received`);
const list = document.createElement('div');
list.classList.add(`${NAME}-list`, 'row');
el.appendChild(list);
data['edge_owner_to_timeline_media']['edges'].forEach(
(el, i) => {
const item = el['node'];
const preview = ig_media_preview(item['media_preview']);
list.innerHTML +=
`<div class="a col ${NAME}-item"` +
` data-gallery="${NAME}-${ID}" data-href="${item['display_url']}" data-toggle="lightbox" data-force="image">` +
`<img id="${NAME}-${ID}-${item['id']}" src="${item['display_url']}" alt="${item['accessibility_caption']}"` +
`style="background:url(${preview})" />` +
'</div>';
},
);
el.classList.remove(`${NAME}-loading`);
el.classList.add(`${NAME}-loaded`);
window.dispatchEvent(new Event('MetaWindowindow.initLinks'));
window.dispatchEvent(new Event(`${NAME}.loaded`));
},
on_error: (e) => {
console.error(`${NAME}: ${e}`);
el.classList.remove(`${NAME}-loading`);
el.classList.add(`${NAME}-error`);
window.dispatchEvent(new Event(`${NAME}.error`));
},
});
});
};
window.addEventListener(`${Events.LODEDANDREADY}`, loadFeed);
window.addEventListener(`${Events.AJAX}`, loadFeed);
})(window);

122
src/js/ui/map.api.js Normal file
View File

@ -0,0 +1,122 @@
'use strict';
import Events from '../_events';
import Consts from 'js/_consts';
import '../../scss/ui/map.api.scss';
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'] = '<i class="fas fa-map-marker-alt"></i>';
}
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;

View File

@ -0,0 +1,3 @@
.site__elements__accordion {
>.element-container>.accordion {}
}

View File

@ -0,0 +1,17 @@
// remove paddings for elemental list cuz inner elements will have paddings
.dnadesign__elementallist__model__elementlist {
margin: 0;
padding-bottom: 0;
.element__content {
--bs-gutter-x: 2rem;
--bs-gutter-y: .5rem;
}
/*.element {
padding-top: $element-reduced-spacer-y;
padding-bottom: $element-reduced-spacer-y;
margin-top: $element-reduced-d-spacer-y;
margin-bottom: $element-reduced-d-spacer-y;
}*/
}

View File

@ -0,0 +1,45 @@
.elemental-area {
display: flex;
flex-direction: column;
--bs-gutter-x: .75rem;
--bs-gutter-y: .5rem;
>.element {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
.element {
position: relative;
margin-top: $element-spacer-y;
margin-bottom: $element-spacer-y;
padding-top: $element-spacer-y;
padding-bottom: $element-spacer-y;
// Sidebar-like elements
&.secondary {
padding-top: ($element-reduced-spacer-y);
padding-bottom: ($element-reduced-spacer-y);
}
}
// sub-elements
.element {
.elemental-area {
.element {
.container,
.container-fluid {
padding: 0;
width: auto;
}
}
}
}

View File

@ -0,0 +1,7 @@
@import 'grid';
@import 'grid.list';
@import 'page';
@import 'slider';
@import 'sidebar';
@import 'accordion';
@import 'other';

View File

@ -0,0 +1,6 @@
/*
* Basic styles for silverstripe-elemental
*/
.blog-post-info {
position: relative;
}

View File

@ -0,0 +1,38 @@
// hide default page title cuz elemental object will be used to display titles
.element__breadcrumbs {
//margin-bottom: calc(-2 * #{inspect($element-spacer-y)});
.container {}
.breadcrumb-link {
text-decoration: none;
&:hover,
&:focus,
&:active,
&.active {
color: $sidebar-nav-link-hover-color;
}
}
.active {
.breadcrumb-link {
color: $sidebar-nav-link-hover-color;
}
}
}
.page-header-element {
--bs-gutter-y: .75rem;
display: none;
//margin-bottom: calc(-1 * #{inspect($element-spacer-y)});
.page-header {
line-height: 1em;
margin-bottom: 0;
}
}
.page-header-element:not(.d-block)+.element {
margin-top: 0;
}

View File

@ -0,0 +1,57 @@
// remove containers for child elements
.sidebar__col {
position: relative;
margin-top: $element-reduced-spacer-y;
margin-bottom: $element-reduced-spacer-y;
}
.content-holder__sidebar {
.row {
.container,
.container-fluid {
padding: 0;
width: auto;
}
}
}
.page-content-sidebar {
// Sidebar elements
.element {
padding-top: ($element-reduced-spacer-y);
padding-bottom: ($element-reduced-spacer-y);
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
}
}
}
.element__widget {}
.widget__Site_Widgets_SubmenuWidget {
.nav-link {
width: 100%;
&:hover,
&:focus,
&:active,
&.active {
font-weight: bold;
color: $sidebar-nav-link-hover-color;
}
}
.active {
.nav-link {
font-weight: bold;
color: $sidebar-nav-link-hover-color;
}
}
}

View File

@ -0,0 +1,33 @@
.dynamic__elements__image__elements__elementimage,
.site__elements__sliderelement {
.element-container {
max-width: none;
padding-left: 0;
padding-right: 0;
}
.element__image {
min-width: 100%;
}
.carousel-slide {
background: $sliderelement-carousel-slide-bg;
max-height: $sliderelement-carousel-slide-max-y;
align-items: center;
.video {
position: relative;
height: 100%;
@include responsive-ratio($sliderelement-carousel-slide-ratio-x, $sliderelement-carousel-slide-ratio-y, true);
iframe {
position: absolute;
top: 0;
height: 100% !important;
width: 100vw !important;
max-width: none;
height: unquote(($sliderelement-carousel-slide-ratio-y / $sliderelement-carousel-slide-ratio-x) * 100 + 'vw') !important;
}
}
}
}

View File

@ -0,0 +1,30 @@
.field {
flex-direction: row;
&__label {
padding-right: $form-spacer-x;
display: inline-flex;
align-items: center;
&+.field__content {
padding-left: $form-spacer-x;
}
}
.field__content {
flex: 1 1 auto;
}
&.CompositeField {
flex-direction: column;
}
}
.field.password {
.show-password {
position: absolute;
top: 0.5em;
right: 0.5em;
color: $input-color;
}
}

View File

@ -0,0 +1 @@
@import './basics';

View File

@ -0,0 +1,5 @@
@import '../_variables';
@import '../_animations';
@import './main';
@import './forms';

View File

@ -0,0 +1,32 @@
@import '../../_variables';
#SiteWideAlerts {
position: fixed;
bottom: 0;
right: 0;
z-index: 99999;
.btn-close {
background: none;
}
.alert {
margin-bottom: 0;
}
}
.alert-offline {
display: none;
}
.is-online {
.alert-offline {
display: none;
}
}
.is-offline {
.alert-offline {
display: flex;
}
}

View File

@ -0,0 +1,179 @@
/*
* some basic styles
*/
@import '../../_variables';
@import '../../_animations';
html,
body {
min-height: 100%;
min-height: 100vh;
}
// sticky footer
body {
display: flex;
flex-direction: column;
--body-gutter-x: #{inspect($body-gutter-x)};
--body-gutter-y: #{inspect($body-gutter-y)};
--body-double-gutter-x: #{inspect($body-double-gutter-x)};
--body-double-gutter-y: #{inspect($body-double-gutter-y)};
--body-gutter-reduced-x: #{inspect($body-gutter-reduced-x)};
--body-gutter-reduced-y: #{inspect($body-gutter-reduced-y)};
--body-gutter-reduced-d-x: #{inspect($body-gutter-reduced-d-x)};
--body-gutter-reduced-d-y: #{inspect($body-gutter-reduced-d-y)};
.wrapper {
flex: 1 0 auto;
margin-bottom: $element-spacer-y;
}
.footer {
flex-shrink: 0;
margin-top: $element-spacer-y;
}
}
@media (min-width: $extra-large-screen) {
html,
body {
font-size: .9vw !important;
}
.container {
max-width: 80vw;
}
}
// don't let images be wider than the parent layer
div,
a,
span,
button,
i {
background-repeat: no-repeat;
background-size: contain;
}
iframe,
img {
max-width: 100%;
}
ul,
table,
p {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
.a {
cursor: pointer;
color: $link-color;
text-decoration: $link-decoration;
&:hover,
&:focus {
text-decoration: $link-hover-decoration;
color: $link-hover-color;
}
}
// exclude bootstrap-table
[data-toggle='table'] {
&:hover,
&.active,
&:focus {
opacity: 1;
}
}
[data-toggle='collapse'] {
&[aria-expanded='true'] {
.accordion-icon {
&:before {
content: '\f068';
}
}
}
}
// transactions
.transition,
a,
a *,
.a,
.a *,
button,
input,
optgroup,
select,
textarea,
.btn,
.btn *,
.dropdown,
.row,
.alert,
.alert *,
.message,
[data-toggle],
[data-toggle] * {
transition: all 0.4s ease;
}
.a,
a,
[data-toggle],
button,
.btn {
&:hover,
&.active,
&[aria-expanded='true'] {
>.fa,
>.far,
>.fas,
>.fab,
&.fa,
&.far,
&.fas,
&.fab {
transform: scale(1.5);
}
}
&:hover,
&[aria-expanded='true'] {
opacity: 0.8;
}
&.disabled {
opacity: 0.5;
cursor: default;
&:hover,
&.active,
&[aria-expanded='true'] {
>.fa,
>.far,
>.fas,
>.fab,
&.fa,
&.far,
&.fas,
&.fab {
transform: rotate(0deg);
}
}
}
}

View File

@ -0,0 +1,9 @@
@import '../../_variables';
@import '../../_animations';
@import './base';
@import './main';
@import './alerts';
// states
@import './states';

View File

@ -0,0 +1,176 @@
/*
* some basic styles
*/
.meta-MetaWindow {
z-index: 1031;
.meta-nav {
text-decoration: none;
}
}
.pulse {
animation: pulse 0.8s linear infinite;
}
// navs
.navbar-toggler {
transition: transform ease 0.4s;
}
.navbar-toggler-icon {
width: auto;
height: auto;
}
.nav-item,
.nav-link {
display: flex;
}
button.nav-link {
border: 0;
outline: 0;
text-transform: inherit;
letter-spacing: inherit;
}
.navbar-toggler {
&[aria-expanded='true'] {
transform: rotate(90deg);
}
}
.dropdown-toggle {
position: relative;
padding-right: 1.5em;
&:after {
position: absolute;
right: 0.5em;
bottom: 1em;
}
}
.navbar-nav .dropdown-toggle.nav-link {
padding-right: 1.5em;
}
.dropdown.show .dropdown-toggle::after,
.dropdown-toggle.active-dropdown::after,
.dropdown-toggle.active::after {
transform: rotate(-90deg);
}
.dropdown-menu {
padding: 0;
border-radius: 0;
will-change: max-height, display;
overflow: hidden;
transition: none;
&.show {
animation: expand 2s;
animation-fill-mode: both;
overflow: visible;
}
.dropdown-list {
@extend .list-unstyled;
}
.dropdown-menu {
top: 0;
left: 100%;
}
}
.dropdown-item {
white-space: normal;
}
.field {
position: relative;
display: flex;
flex-wrap: wrap;
margin: $form-spacer-y 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
.btn-toolbar {
margin-top: $form-spacer-y;
}
// rewrite btn opacity on hover
.btn {
&:hover,
&.active,
&:focus {
opacity: 1;
}
}
// SS-messages
.alert+.alert {
border-top: 0;
}
/*.message {
@extend .alert;
@extend .alert-info;
display: block;
margin: 0.5rem 0;
}
.message.validation,
.message.required,
.message.error {
@extend .alert;
@extend .alert-danger;
}
.message.required,
.message.error {
@extend .alert;
@extend .alert-danger;
}*/
.list-group-item.active {
a,
.a {
color: $list-group-active-color;
}
}
[aria-expanded='true'] {
.fa-bars {
&:before {
content: '\f00d';
}
}
}
.jsSidebarUI {
position: relative;
min-height: 100%;
}
.jsSidebarUI__inner {
position: relative;
will-change: position, top;
}

View File

@ -0,0 +1,5 @@
@import '../../../_variables';
@import '../../../_animations';
@import './mobile';
@import './network';

View File

@ -0,0 +1,43 @@
/*
* Mobile/Desktop states
*/
// display dropdown on hover + focus
@media (min-width: $full-body-min-width) {
.dropdown-hover {
&:hover,
&:focus {
.dropdown-menu {
display: block;
}
}
}
}
// custom toggler for mobile view
.dropdown {
>.dropdown-toggle-sm {
@media (min-width: $full-body-min-width) {
display: none;
}
}
>.dropdown-toggle-fl {
display: none;
@media (min-width: $full-body-min-width) {
display: inherit;
}
}
@media not all and (hover: none) {
>.dropdown-toggle-touch {
display: inherit;
}
>.dropdown-toggle-notouch {
display: none;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Network States
*/
.loading {
animation: fade 0.5s linear infinite;
}
.graphql-page {
&.response-404 {
filter: grayscale(1);
opacity: 0.5;
cursor: not-allowed;
}
}
.is-offline {
iframe {
display: none;
}
.graphql-page {
&.response-523 {
filter: grayscale(1);
opacity: 0.5;
cursor: not-allowed;
}
}
}
body.ajax-loading {
overflow: hidden;
height: 100vh;
#Header {
position: relative;
z-index: 2001;
}
}

177
src/scss/layout/test.scss Normal file
View File

@ -0,0 +1,177 @@
.sidebar__col {
position: relative;
margin-top: $element-reduced-spacer-y;
margin-bottom: $element-reduced-spacer-y;
}
.content-holder__sidebar {
> .container {
padding: 0;
}
}
#SiteWideMessage {
text-align: center;
.alert {
margin-bottom: 0;
.btn-close {
margin-top: -0.5rem;
float: right;
}
}
}
#Header {
background-color: $header-bg;
color: $header-color;
a {
color: $header-link;
}
.nav-container {
display: flex;
justify-content: flex-end;
align-items: flex-end;
position: static;
}
.logo {
filter: invert(100%);
}
.tagline {
display: inline-block;
font-size: 1.2rem;
margin-left: 2em;
}
}
#Navigation {
font-size: 1.5rem;
text-transform: uppercase;
letter-spacing: 0.25rem;
width: 100%;
background: $header-bg;
.navbar-toggler {
color: $main-nav-link-color;
font-size: $main-nav-toggler-size;
}
.nav-item,
.nav-link {
flex-direction: column;
/*@media (min-width: $full-body-min-width) {
align-items: center;
justify-content: center;
text-align: center;
}*/
}
.nav-link {
color: $main-nav-link-color;
background: $main-nav-link-bg;
&:focus,
&:hover,
&.active {
background: $main-nav-link-hover-bg;
color: $main-nav-link-hover-color;
}
}
.active {
> .nav-link {
background: $main-nav-link-hover-bg;
color: $main-nav-link-hover-color;
}
}
.nav-item .nav-dropdown {
.fa-chevron-right
//&:after
{
display: none;
}
}
.dropdown-menu {
border-color: $main-nav-dropdown-bg;
background: $main-nav-dropdown-bg;
margin-top: 0;
border-top: 0;
min-width: 100%;
.nav-item-link {
color: $main-nav-dropdown-color;
}
}
.dropdown-item {
&.active,
&:active,
&:focus,
&:hover {
background: $main-nav-dropdown-hover-bg;
.nav-item-link {
color: $main-nav-dropdown-hover-color;
}
}
.nav-item-link {
width: 100%;
justify-content: flex-start;
align-items: flex-start;
}
}
@media (min-width: $full-body-min-width) {
.navbar-nav > .nav-item {
padding-right: 2rem;
padding-left: 2rem;
}
.dropdown-item .nav-item-link {
padding-left: 1rem;
padding-right: 1rem;
}
}
}
/*#MainContent {
padding-top: 2 * $element-reduced-spacer-y;
padding-bottom: 2 * $element-reduced-spacer-y;
}*/
#PageBreadcumbs {
position: relative;
z-index: 2;
}
#Footer {
display: flex;
flex-direction: column;
background-color: $footer-bg;
color: $footer-color;
> .wrapper {
padding-top: $element-reduced-spacer-y;
padding-bottom: $element-reduced-spacer-y;
}
a,
.a {
color: $footer-link;
}
.footer {
padding-top: $element-reduced-spacer-y;
padding-bottom: $element-reduced-spacer-y;
background-color: $footer-footer-bg;
.copyright {
padding-right: 0.5rem;
}
li {
padding: 0 0.5rem;
}
}
}

39
src/scss/libs/bootstrap-table.scss vendored Normal file
View File

@ -0,0 +1,39 @@
@import "~bootstrap-table/src/bootstrap-table.scss";
.bootstrap-table {
.fixed-table-container {
.table {
thead th {
.both, .asc, .desc {
background-image: none;
&:after {
margin-left: .5em;
content: '';
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
}
.asc:after {
content: "\f0de";
}
.desc:after {
content: "\f0dd";
}
.both:after {
content: "\f0dc";
}
.th-inner.sortable {
&:hover,
&:focus {
opacity: .8;
}
}
}
}
}
}

51
src/scss/libs/bootstrap.scss vendored Normal file
View File

@ -0,0 +1,51 @@
// Bootstrap
// Configuration
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import '~bootstrap/scss/mixins';
@import '~bootstrap/scss/utilities';
// Layout & components
@import '~bootstrap/scss/root';
@import '~bootstrap/scss/reboot';
@import '~bootstrap/scss/type';
@import '~bootstrap/scss/containers';
@import '~bootstrap/scss/grid';
@import '~bootstrap/scss/tables';
@import '~bootstrap/scss/forms';
@import '~bootstrap/scss/buttons';
@import '~bootstrap/scss/transitions';
// Optional
//@import '~bootstrap/scss/images';
@import '~bootstrap/scss/dropdown';
@import '~bootstrap/scss/nav';
@import '~bootstrap/scss/navbar';
@import '~bootstrap/scss/breadcrumb';
@import '~bootstrap/scss/pagination';
@import '~bootstrap/scss/alert';
@import '~bootstrap/scss/close';
/*@import '~bootstrap/scss/button-group';
@import '~bootstrap/scss/card';
@import '~bootstrap/scss/accordion';
@import '~bootstrap/scss/badge';
@import '~bootstrap/scss/progress';
@import '~bootstrap/scss/list-group';
@import '~bootstrap/scss/toasts';
@import '~bootstrap/scss/modal';
@import '~bootstrap/scss/tooltip';
@import '~bootstrap/scss/popover';
@import '~bootstrap/scss/spinners';*/
// Helpers
@import '~bootstrap/scss/helpers';
// Utilities
@import '~bootstrap/scss/utilities/api';
@import '../ui/carousel';
.navbar {
justify-content: flex-end;
}

3
src/scss/libs/fontawesome.scss vendored Normal file
View File

@ -0,0 +1,3 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";

View File

@ -0,0 +1,57 @@
.message {
@extend .alert !optional;
&.warning {
@extend .alert-warning !optional;
}
&.error {
@extend .alert-danger !optional;
}
}
.embed-responsive-4by3,
.embed-responsive-16by9 {
position: relative;
padding-top: 56.25%;
iframe {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
}
.embed-responsive-4by3 {
padding-top: 75%;
}
#ForgotPassword {
margin: 1rem 0;
width: 100%;
}
#BetterNavigator {
display: none;
top: 50% !important;
margin-top: -41px;
&.open {
top: 0 !important;
margin-top: 0;
}
a,
button,
div,
i,
span {
background-size: auto;
}
@media (min-width: map-get($grid-breakpoints, 'md')) {
display: block;
}
}

View File

@ -0,0 +1,8 @@
.cart-footer {
margin-top: $grid-gutter-height / 2;
}
.address-panel,
.account-nav {
margin-bottom: $grid-gutter-height / 2;
}

93
src/scss/ui/carousel.scss Normal file
View File

@ -0,0 +1,93 @@
@import '~bootstrap/scss/carousel';
/*
* Bootstrap carousel improvement
*/
/*.carousel-item {
&.active {
display: flex !important;
justify-content: center;
align-items: flex-start;
}
}*/
$carousel-title-color: $white !default;
$carousel-slide-min-height: 4rem !default;
$carousel-text-shadow: 1px 1px $black !default;
$carousel-controls-font-size: 3rem;
$carousel-controls-zindex: 11 !default;
$carousel-controls-shadow: 1px 1px $black !default;
$carousel-controls-hover-bg: transparentize($black, 0.4) !default;
$carousel-slide-img-loading-max-height: 25vh !default;
.carousel-slide {
min-height: $carousel-slide-min-height;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
>.container {
position: relative;
}
.video {
width: 100%;
iframe {
width: 100% !important;
height: auto !important;
}
}
.img {
display: block;
width: 100%;
}
img.loading {
max-height: $carousel-slide-img-loading-max-height;
}
}
.carousel-control-prev,
.carousel-control-next {
z-index: $carousel-controls-zindex;
font-size: $carousel-controls-font-size;
text-shadow: $carousel-controls-shadow;
&:hover,
&:focus {
background: $carousel-controls-hover-bg;
}
}
.carousel-indicators li {
box-shadow: none;
}
.carousel-title {
color: $carousel-title-color;
}
.carousel-title,
.carousel-content {
text-shadow: $carousel-text-shadow;
}
.carousel-caption {
right: 0;
left: auto;
width: 50%;
bottom: 0;
}
.slide-link__media {
position: absolute;
opacity: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
}

34
src/scss/ui/flyout.scss Normal file
View File

@ -0,0 +1,34 @@
$flyout-height-padding: 1rem;
$flyout-width-padding: 2rem;
$flyout-padding: $flyout-height-padding $flyout-width-padding;
$flyout-bg: #000 !default;
$flyout-color: #fff !default;
$flyout-title-color: #fff !default;
$flyout-transition: right 2s;
.flyout-FlyoutUI {
position: absolute;
z-index: 99;
transform: translateY(-50%);
transition: $flyout-transition;
right: -100%;
top: 50%;
background: $flyout-bg;
color: $flyout-color;
padding: $flyout-padding;
&__active {
display: block;
right: 0;
}
&__title {
color: $flyout-title-color;
}
&__close {
position: absolute;
top: $flyout-height-padding;
right: $flyout-width-padding;
}
}

View File

@ -0,0 +1,9 @@
.form-stepped {
.step {
display: none !important;
&.active {
display: flex !important;
}
}
}

14
src/scss/ui/lightbox.scss Normal file
View File

@ -0,0 +1,14 @@
@import '../_variables';
/*$lightbox-breakpoint: map-get($grid-breakpoints, 'sm') !default;
$lightbox-link-hover-color: $link-hover-color !default;
@import '~@a2nt/meta-lightbox/src/scss/app';
.lightbox-overlay-custom {
@extend .meta-lightbox-overlay;
@extend .meta-lightbox-theme-default;
@extend .meta-lightbox-effect-fade;
// meta-lightbox-open
}
*/

View File

@ -0,0 +1,53 @@
#mc_embed_signup,
.mc_embed_signup {
padding: 2rem;
.mc-field-group {
@extend .form-group;
}
input[type='text'],
input[type='email'] {
@extend .form-control;
}
input[type='submit'] {
@extend .btn;
@extend .btn-primary;
margin: 0 auto;
width: 50%;
display: block;
}
.clear {
float: none;
clear: both;
}
.input-group {
@extend .form-check;
ul,
li {
list-style: none;
}
input[type='checkbox'] {
@extend .form-check-input;
}
label {
@extend .form-check-label;
}
}
.mce_inline_error,
#mce-success-response,
#mce-error-response {
margin-top: 1rem;
@extend .alert;
}
#mce-success-response {
@extend .alert-success;
}
.mce_inline_error,
#mce-error-response {
@extend .alert-danger;
}
}

134
src/scss/ui/map.api.scss Normal file
View File

@ -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%;
}
}
}

View File

@ -0,0 +1,55 @@
@import '../_variables';
$grid-gutter-element-height: 2rem !default;
.jsMultiSlider {
position: relative;
display: flex;
margin-bottom: $grid-gutter-element-height/2;
align-items: center;
justify-content: center;
min-width: 100%;
&-active {
margin-bottom: 0;
}
.slide {
position: relative;
padding: 0 0.5rem;
}
}
.jsMultiSlider-container {
position: relative;
margin-bottom: $grid-gutter-element-height/2;
.slider-actions {
font-size: 2rem;
.act {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
&-slider-prev {
}
&-slider-next {
left: auto;
right: 0;
}
}
}
}
.jsMultiSlider-slides-container {
overflow: hidden;
margin: 0 2rem;
> .slider-nav {
position: relative;
}
}