diff --git a/src/js/_components/_apollo.cache.js b/src/js/_components/_apollo.cache.js new file mode 100644 index 0000000..6599de2 --- /dev/null +++ b/src/js/_components/_apollo.cache.js @@ -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 }; diff --git a/src/js/_components/_element.jsx b/src/js/_components/_element.jsx new file mode 100644 index 0000000..12172a8 --- /dev/null +++ b/src/js/_components/_element.jsx @@ -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) => ( +
+ ); + + let html = ''; + if (ui.state.Elements.length) { + ui.state.Elements.map((el) => { + html += el.node.Render; + }); + } else { + html += '
Loading ...
'; + } + + return ( +
+ ); + } +} + +export default Page; diff --git a/src/js/_components/_main.css-screen-size.js b/src/js/_components/_main.css-screen-size.js new file mode 100644 index 0000000..c1749e3 --- /dev/null +++ b/src/js/_components/_main.css-screen-size.js @@ -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); diff --git a/src/js/_components/_main.links.js b/src/js/_components/_main.links.js new file mode 100644 index 0000000..5414822 --- /dev/null +++ b/src/js/_components/_main.links.js @@ -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( + , + 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( + , + 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; diff --git a/src/js/_components/_main.online.js b/src/js/_components/_main.online.js new file mode 100644 index 0000000..1fbdb31 --- /dev/null +++ b/src/js/_components/_main.online.js @@ -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); diff --git a/src/js/_components/_main.touch.js b/src/js/_components/_main.touch.js new file mode 100644 index 0000000..9828a20 --- /dev/null +++ b/src/js/_components/_main.touch.js @@ -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); diff --git a/src/js/_components/_main.visibility.js b/src/js/_components/_main.visibility.js new file mode 100644 index 0000000..8459a35 --- /dev/null +++ b/src/js/_components/_main.visibility.js @@ -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);