mirror of
https://github.com/a2nt/webpack-bootstrap-ui-kit.git
synced 2024-10-22 11:05:45 +02:00
IMPR: GraphQL page browsing with History API + Minor logical Improvements
This commit is contained in:
parent
cd305e05cb
commit
025cb00bc1
19
src/js/_components/_apollo.cache.js
Normal file
19
src/js/_components/_apollo.cache.js
Normal file
@ -0,0 +1,19 @@
|
||||
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 };
|
208
src/js/_components/_element.jsx
Normal file
208
src/js/_components/_element.jsx
Normal file
@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Lightbox window
|
||||
*/
|
||||
import { Component } from 'react';
|
||||
import Events from '../_events';
|
||||
|
||||
import { client } from './_apollo';
|
||||
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;
|
54
src/js/_components/_main.css-screen-size.js
Normal file
54
src/js/_components/_main.css-screen-size.js
Normal 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);
|
146
src/js/_components/_main.links.js
Normal file
146
src/js/_components/_main.links.js
Normal file
@ -0,0 +1,146 @@
|
||||
// browser tab visibility state detection
|
||||
|
||||
import Events from '../_events';
|
||||
import Consts from '../_consts';
|
||||
import Page from './_page.jsx';
|
||||
|
||||
const MainUILinks = ((W) => {
|
||||
const NAME = '_main.links';
|
||||
const D = document;
|
||||
const BODY = D.body;
|
||||
|
||||
class MainUILinks {
|
||||
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 reset() {
|
||||
// reset focus
|
||||
D.activeElement.blur();
|
||||
|
||||
// remove active and loading classes
|
||||
D.querySelectorAll('.graphql-page,.nav-item').forEach((el2) => {
|
||||
el2.classList.remove('active');
|
||||
el2.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
|
||||
static popState(e) {
|
||||
const ui = this;
|
||||
|
||||
if (!ui.GraphPage) {
|
||||
console.log(
|
||||
`${NAME}: [popstate] GraphPage is missing. Have to render it first`,
|
||||
);
|
||||
|
||||
ui.GraphPage = ReactDOM.render(
|
||||
<Page />,
|
||||
document.getElementById('MainContent'),
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
D.querySelectorAll(`[data-${ui.name}-id="${e.state.link}"]`).forEach(
|
||||
(el) => {
|
||||
el.classList.add('active');
|
||||
},
|
||||
);
|
||||
|
||||
ui.GraphPage.setState(state);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// link specific event {this} = current link, not MainUILinks
|
||||
static loadClick(e) {
|
||||
console.groupCollapsed(`${NAME}: load on click`);
|
||||
const ui = MainUILinks;
|
||||
ui.reset();
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (!ui.GraphPage) {
|
||||
ui.GraphPage = ReactDOM.render(
|
||||
<Page />,
|
||||
document.getElementById('MainContent'),
|
||||
);
|
||||
}
|
||||
|
||||
const el = e.currentTarget;
|
||||
const link = el.getAttribute('href') || el.getAttribute('data-href');
|
||||
|
||||
ui.GraphPage.state.current = el;
|
||||
|
||||
el.classList.add('loading');
|
||||
ui.GraphPage.load(link)
|
||||
.then((response) => {
|
||||
el.classList.remove('loading');
|
||||
el.classList.add('active');
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
console.groupEnd(`${NAME}: load on click`);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
|
||||
el.classList.remove('loading');
|
||||
el.classList.add('not-found');
|
||||
|
||||
console.groupEnd(`${NAME}: load on click`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
W.addEventListener(`${Events.LOADED}`, () => {
|
||||
MainUILinks.init();
|
||||
});
|
||||
|
||||
W.addEventListener(`${Events.AJAX}`, () => {
|
||||
MainUILinks.loaded();
|
||||
});
|
||||
})(window);
|
||||
|
||||
export default MainUILinks;
|
101
src/js/_components/_main.online.js
Normal file
101
src/js/_components/_main.online.js
Normal file
@ -0,0 +1,101 @@
|
||||
// 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
|
||||
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}`, () => {
|
||||
UPDATE_ONLINE_STATUS(true);
|
||||
});
|
||||
})(window);
|
70
src/js/_components/_main.touch.js
Normal file
70
src/js/_components/_main.touch.js
Normal 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/_components/_main.visibility.js
Normal file
34
src/js/_components/_main.visibility.js
Normal 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);
|
Loading…
Reference in New Issue
Block a user