531 lines
18 KiB
JavaScript
Executable File
531 lines
18 KiB
JavaScript
Executable File
import { Helper, InitPromise, Fragment, Translator } from './pwa-lib.js';
|
|
|
|
class DelayPromise extends Promise {
|
|
static async delay(delay) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, delay);
|
|
});
|
|
}
|
|
}
|
|
|
|
class InstallManager {
|
|
static init() {
|
|
window.addEventListener('beforeinstallprompt', e => {
|
|
e.preventDefault();
|
|
this.setDeferredPrompt(e);
|
|
});
|
|
}
|
|
|
|
static setDeferredPrompt(e){
|
|
this.deferredPromt = e;
|
|
if (this.canInstallListener) {
|
|
this.canInstallListener(this.deferredPromt);
|
|
}
|
|
}
|
|
|
|
static async prompt(){
|
|
if (Helper.isNotNull(this.deferredPromt)){
|
|
this.deferredPromt.prompt();
|
|
return this.deferredPromt.userChoice();
|
|
}
|
|
return Promise.resolve({
|
|
"outcome":"dismissed",
|
|
"platform":""
|
|
});
|
|
}
|
|
|
|
static setCanInstallListener(listener, callIfCanInstall) {
|
|
this.canInstallListener = listener;
|
|
callIfCanInstall = Helper.nonNull(callIfCanInstall, true);
|
|
|
|
if (callIfCanInstall && Helper.nonNull(this.deferredPromt)) {
|
|
this.canInstallListener(this.deferredPromt);
|
|
}
|
|
}
|
|
}
|
|
|
|
InstallManager.init();
|
|
|
|
class Matomo {
|
|
|
|
static init() {
|
|
Matomo.isTrackingPromise = new Promise(async (resolve) => {
|
|
let shouldTrack = Helper.nonNull(localStorage.getItem(Matomo.LOCAL_STORAGE_KEY), "1");
|
|
if (Helper.isNull(shouldTrack)) {
|
|
shouldTrack = await Matomo._askIsTracking();
|
|
localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, shouldTrack);
|
|
}
|
|
else {
|
|
shouldTrack = (shouldTrack === "1");
|
|
Matomo.setTrack(shouldTrack);
|
|
}
|
|
resolve(shouldTrack);
|
|
});
|
|
Matomo.isTrackingPromise.then(() => {
|
|
Matomo.push(['trackPageView'], true);
|
|
Matomo.push(['enableLinkTracking'], true);
|
|
Matomo.push(['setTrackerUrl', Matomo.TRACK_SITE + '/piwik.php'], true);
|
|
Matomo.push(['setSiteId', Matomo.SIDE_ID + ""], true);
|
|
|
|
let d = document, g = d.createElement('script'), s = d.getElementsByTagName('head')[0];
|
|
g.type = 'text/javascript';
|
|
g.async = true;
|
|
g.defer = true;
|
|
g.src = Matomo.TRACK_SITE + '/piwik.js';
|
|
s.appendChild(g);
|
|
});
|
|
// window.addEventListener('hashchange', () => {
|
|
// Matomo.update()
|
|
// });
|
|
}
|
|
|
|
static update(title) {
|
|
if (Helper.nonNull(Matomo.currentUrl)){
|
|
Matomo.push(['setReferrerUrl', Matomo.currentUrl]);
|
|
}
|
|
Matomo.currentUrl = window.location.pathname+window.location.search;
|
|
Matomo.push(['setCustomUrl', Matomo.currentUrl]);
|
|
Matomo.push(['setDocumentTitle', title]);
|
|
|
|
// remove all previously assigned custom variables, requires Matomo (formerly Piwik) 3.0.2
|
|
Matomo.push(['deleteCustomVariables', 'page']);
|
|
Matomo.push(['setGenerationTimeMs', 0]);
|
|
Matomo.push(['trackPageView']);
|
|
|
|
// make Matomo aware of newly added content
|
|
var content = document.getElementById('site-content');
|
|
Matomo.push(['MediaAnalytics::scanForMedia', content]);
|
|
Matomo.push(['FormAnalytics::scanForForms', content]);
|
|
Matomo.push(['trackContentImpressionsWithinNode', content]);
|
|
Matomo.push(['enableLinkTracking']);
|
|
}
|
|
|
|
static async _askIsTracking() {
|
|
Matomo.isTrackingPromise = new Promise(resolve => {
|
|
Matomo.push([function () {
|
|
resolve(!this["isUserOptedOut"]());
|
|
}]);
|
|
Matomo.push([function () {
|
|
resolve(!this["isUserOptedOut"]());
|
|
}]);
|
|
});
|
|
// Matomo.isTrackingPromise = Matomo.query("isTracked")
|
|
// .then(xml => {
|
|
// let textContent = xml.firstChild.textContent;
|
|
// // localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, textContent);
|
|
// return (textContent === "1")
|
|
// });
|
|
return Matomo.isTrackingPromise;
|
|
}
|
|
|
|
static
|
|
async query(method) {
|
|
return fetch(Matomo.TRACK_SITE + Matomo.BASE_PATH + method, {
|
|
"mode": "cors",
|
|
"credentials": "include",
|
|
}).then(res => res.text()).then(text => (new window.DOMParser()).parseFromString(text, "text/xml"));
|
|
}
|
|
|
|
static getTrackingPromise() {
|
|
return Matomo.isTrackingPromise;
|
|
}
|
|
|
|
static
|
|
async setTrack(shouldTrack) {
|
|
Matomo.isTrackingPromise = Promise.resolve(shouldTrack);
|
|
localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, (shouldTrack === true) ? "1" : "0");
|
|
// return await Matomo.query((shouldTrack) ? "doTrack" : "doIgnore");
|
|
if (shouldTrack) {
|
|
Matomo.push(["forgetUserOptOut"], true);
|
|
}
|
|
else {
|
|
Matomo.push(["optUserOut"], true);
|
|
}
|
|
}
|
|
|
|
static
|
|
async push(arr, force) {
|
|
// force = Helper.nonNull(force, false);
|
|
|
|
if (!Array.isArray(arr)) {
|
|
arr = [arr];
|
|
}
|
|
// if (force || await Matomo.getTrackingPromise()) {
|
|
window["_paq"].push(arr);
|
|
// }
|
|
}
|
|
}
|
|
|
|
Matomo.currentUrl = null;
|
|
|
|
Matomo.LOCAL_STORAGE_KEY = "matomoShouldTrack";
|
|
Matomo.TRACK_SITE = "//matomo.silas.link";
|
|
Matomo.BASE_PATH = "/index.php?module=API&method=AjaxOptOut.";
|
|
Matomo.SIDE_ID = "1";
|
|
|
|
InitPromise.addPromise(() => {
|
|
window["_paq"] = window["_paq"] || [];
|
|
Matomo.init();
|
|
});
|
|
|
|
class RotateHelper {
|
|
rotate(element, degrees){
|
|
let rotateText = element.innerText;
|
|
element.removeAllChildren();
|
|
|
|
let partDegree = degrees/rotateText.length;
|
|
for(let i = 0; i < rotateText.length; i++){
|
|
let child = document.createElement("span");
|
|
child.innerText = rotateText.charAt(i);
|
|
child.style.transform ="rotate("+(partDegree*i)+"deg)";
|
|
child.classList.add("rotated");
|
|
element.appendChild(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ScaleHelper {
|
|
async scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, addListener) {
|
|
|
|
addListener = Helper.nonNull(addListener, true);
|
|
animationDelay = Helper.nonNull(animationDelay, 0);
|
|
|
|
let newFontSize = await this._getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay === 0);
|
|
|
|
if (animationDelay > 0) {
|
|
await new Promise(r => {
|
|
setTimeout(r, animationDelay);
|
|
fontElement.style.fontSize = newFontSize + "px";
|
|
});
|
|
}
|
|
|
|
let self = this;
|
|
let listener = function () {
|
|
return new Promise(resolve => {
|
|
let timeout = (typeof addListener === 'number') ? addListener : 255;
|
|
setTimeout(() => {
|
|
resolve(self.scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, false));
|
|
}, timeout);
|
|
});
|
|
};
|
|
if (addListener !== false) {
|
|
window.addEventListener("resize", listener);
|
|
}
|
|
return listener;
|
|
}
|
|
|
|
async scaleToFull(fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener) {
|
|
return this.scaleTo(1, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener);
|
|
}
|
|
|
|
async _getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, setFontSize) {
|
|
margin = Helper.nonNull(margin, 10);
|
|
ignoreHeight = Helper.nonNull(ignoreHeight, false);
|
|
ignoreWidth = Helper.nonNull(ignoreWidth, false);
|
|
fontWeight = Helper.nonNull(fontWeight, fontElement.innerHTML.length);
|
|
setFontSize = Helper.nonNull(setFontSize, true);
|
|
|
|
let hasNoTransitionClass = container.classList.contains("no-transition");
|
|
|
|
if (!hasNoTransitionClass) {
|
|
container.classList.add("no-transition");
|
|
}
|
|
|
|
let beforeFontSize = fontElement.style.fontSize;
|
|
let currentFontSize = 1;
|
|
let diff = 0;
|
|
let widthDiff = 0;
|
|
let heightDiff = 0;
|
|
let containerWidth = 0;
|
|
let containerHeight = 0;
|
|
do {
|
|
currentFontSize += diff / (fontWeight + 1);
|
|
fontElement.style.fontSize = currentFontSize + 'px';
|
|
|
|
let containerStyle = window.getComputedStyle(container);
|
|
|
|
containerWidth = containerStyle.getPropertyValue("width").replace('px', '');
|
|
containerHeight = containerStyle.getPropertyValue("height").replace('px', '');
|
|
|
|
widthDiff = containerWidth - fontElement.offsetWidth;
|
|
heightDiff = containerHeight - fontElement.offsetHeight;
|
|
|
|
let newDiff = (ignoreWidth ? heightDiff : (ignoreHeight ? widthDiff : Math.min(widthDiff, heightDiff)));
|
|
if (newDiff === diff) {
|
|
break;
|
|
}
|
|
diff = newDiff;
|
|
} while ((widthDiff > (1 - scale) * containerWidth || ignoreWidth) && (heightDiff > (1 - scale) * containerHeight || ignoreHeight));
|
|
|
|
currentFontSize -= margin;
|
|
fontElement.style.fontSize = ((setFontSize) ? currentFontSize + "px" : beforeFontSize);
|
|
|
|
if (!hasNoTransitionClass) {
|
|
await new Promise((r) => {
|
|
setTimeout(r, 50);
|
|
});
|
|
container.classList.remove("no-transition");
|
|
}
|
|
|
|
return currentFontSize;
|
|
}
|
|
}
|
|
|
|
class AudioChain {
|
|
|
|
constructor(context, sourceBuffer, chainFunction) {
|
|
this.buffer = sourceBuffer;
|
|
this.shouldLoop = false;
|
|
this.loopStart = null;
|
|
this.loopEnd = null;
|
|
this.chainFunction = chainFunction;
|
|
this.context = context;
|
|
|
|
this.startTime = null;
|
|
this.pauseTime = null;
|
|
this.source = null;
|
|
|
|
this.running = false;
|
|
}
|
|
|
|
setBuffer(buffer) {
|
|
this.buffer = buffer;
|
|
}
|
|
|
|
setLooping(shouldLoop, loopStart, loopEnd) {
|
|
this.shouldLoop = shouldLoop;
|
|
|
|
if (Helper.isNotNull(loopStart)) {
|
|
this.loopStart = loopStart;
|
|
}
|
|
if (Helper.isNotNull(loopEnd)) {
|
|
this.loopEnd = loopEnd;
|
|
}
|
|
}
|
|
|
|
async start(delay, offset, duration) {
|
|
let source = this.context.createBufferSource();
|
|
|
|
source.loop = this.shouldLoop;
|
|
if (Helper.isNotNull(this.loopStart)) {
|
|
source.loopStart = this.loopStart;
|
|
}
|
|
if (Helper.isNotNull(this.loopEnd)) {
|
|
source.loopEnd = this.loopEnd;
|
|
}
|
|
source.buffer = this.buffer;
|
|
await this.chainFunction(source);
|
|
|
|
source.start(delay, offset, duration);
|
|
this.startTime = (new Date()).getTime();
|
|
this.source = source;
|
|
this.running = true;
|
|
}
|
|
|
|
async stop(delay) {
|
|
if (Helper.isNotNull(this.source)) {
|
|
this.pauseTime = ((new Date()).getTime()) - this.startTime;
|
|
return this.source.stop(delay);
|
|
}
|
|
this.running = false;
|
|
return null;
|
|
}
|
|
|
|
async resume() {
|
|
if (!this.running) {
|
|
return this.start(null, Helper.nonNull(this.pauseTime, 0) / 1000.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
class SoundManager {
|
|
static getInstance() {
|
|
if (Helper.isNull(SoundManager.instance)) {
|
|
SoundManager.instance = new SoundManager();
|
|
}
|
|
return SoundManager.instance;
|
|
}
|
|
|
|
constructor() {
|
|
this.channels = {};
|
|
this.context = new AudioContext();
|
|
|
|
window.addEventListener("visibilitychange", () => {
|
|
this.handleVisibilityChange();
|
|
});
|
|
}
|
|
|
|
set(options, channel) {
|
|
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
|
|
let audioObject = Helper.nonNull(this.channels[channel], {});
|
|
|
|
if (typeof options === "string") {
|
|
options = {audio: options};
|
|
}
|
|
|
|
let audio = options.audio;
|
|
if (Helper.isNotNull(audio)) {
|
|
audioObject.loadedPromise = fetch(audio).then(res => res.arrayBuffer()).then(arrayBuffer => this.context.decodeAudioData(arrayBuffer));
|
|
this.stop(channel);
|
|
}
|
|
audioObject.muted = Helper.nonNull(options.muted, audioObject.muted, false);
|
|
audioObject.volume = Helper.nonNull(options.volume, audioObject.volume, 1);
|
|
audioObject.loop = Helper.nonNull(options.loop, audioObject.loop, false);
|
|
audioObject.timeOffset = Helper.nonNull(options.timeOffset, audioObject.timeOffset, 0);
|
|
this.channels[channel] = audioObject;
|
|
|
|
if (audioObject.muted) {
|
|
this.stop(channel);
|
|
}
|
|
|
|
return this.channels[channel];
|
|
}
|
|
|
|
async play(channel, audioOrOptions) {
|
|
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
|
|
if (Helper.isNull(audioOrOptions)) {
|
|
audioOrOptions = {};
|
|
} else if (!(typeof audioOrOptions === "object")) {
|
|
audioOrOptions = {
|
|
audio: audioOrOptions
|
|
};
|
|
}
|
|
audioOrOptions.timeOffset = Helper.nonNull(audioOrOptions.timeOffset, 0);
|
|
|
|
this.stop(channel);
|
|
this.set(audioOrOptions, channel);
|
|
|
|
if (!this.channels[channel].muted) {
|
|
let buffer = await this.channels[channel].loadedPromise;
|
|
let source = new AudioChain(this.context, buffer, (sourceNode) => {
|
|
let gain = this.context.createGain();
|
|
gain.gain.value = this.channels[channel].volume;
|
|
|
|
sourceNode.connect(gain);
|
|
gain.connect(this.context.destination);
|
|
});
|
|
|
|
source.setBuffer(buffer);
|
|
|
|
//to prevent gap in mp3-files
|
|
source.setLooping(this.channels[channel].loop, 0.3, buffer.duration - 0.3);
|
|
|
|
source.start();
|
|
|
|
this.channels[channel].source = source;
|
|
}
|
|
return this.channels[channel];
|
|
}
|
|
|
|
stop(channel) {
|
|
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
|
|
|
|
let oldAudio = this.channels[channel];
|
|
if (Helper.nonNull(oldAudio) && Helper.nonNull(oldAudio.source)) {
|
|
oldAudio.source.stop();
|
|
}
|
|
}
|
|
|
|
get(channel) {
|
|
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
|
|
return this.channels[channel];
|
|
}
|
|
|
|
async resume(channel){
|
|
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
|
|
if (!this.channels[channel].muted && Helper.isNotNull(this.channels[channel].source)) {
|
|
return this.channels[channel].source.resume();
|
|
}
|
|
}
|
|
|
|
handleVisibilityChange() {
|
|
if (document.hidden) {
|
|
for (let k in this.channels) {
|
|
if (Helper.isNotNull(this.channels[k].source)) {
|
|
this.channels[k].source.stop();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (let k in this.channels) {
|
|
if (!this.channels[k].muted && Helper.isNotNull(this.channels[k].source)) {
|
|
this.channels[k].source.resume();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SoundManager.CHANNELS = {
|
|
MUSIC: "music",
|
|
SOUND: "sound",
|
|
DEFAULT: "default"
|
|
};
|
|
|
|
class TabbedFragment extends Fragment {
|
|
constructor(site) {
|
|
super(site, 'pwaAssets/html/fragment/tabbedFragment.html');
|
|
this.fragments = {};
|
|
this.tabHeadingTemplate = null;
|
|
this.tabHeadingContainer = null;
|
|
this.tabContainer = null;
|
|
this.tabKeys = [];
|
|
this.currentTabIndex = 0;
|
|
}
|
|
|
|
onFirstStart() {
|
|
super.onFirstStart();
|
|
this.tabContainer = this.findBy(".tab-container");
|
|
this.showTab(0);
|
|
}
|
|
|
|
addFragment(name, fragment, translationArgs, isTranslatable) {
|
|
isTranslatable = Helper.nonNull(isTranslatable, true);
|
|
if (translationArgs === false) {
|
|
isTranslatable = false;
|
|
}
|
|
|
|
this.fragments[name] = fragment;
|
|
const tabIndex = this.tabKeys.length;
|
|
this.tabKeys.push(name);
|
|
|
|
const _self = this;
|
|
this.inflatePromise = this.inflatePromise.then(function (siteContent) {
|
|
if (Helper.isNull(_self.tabHeadingTemplate)) {
|
|
_self.tabHeadingTemplate = siteContent.querySelector(".tab-header-template");
|
|
}
|
|
const newTabHeader = Helper.cloneNode(_self.tabHeadingTemplate);
|
|
newTabHeader.classList.add("tab-" + tabIndex);
|
|
newTabHeader.querySelector(".tab-name").appendChild((isTranslatable) ? Translator.makePersistentTranslation(name, translationArgs) : document.createTextNode(name));
|
|
newTabHeader.addEventListener("click", function(){
|
|
_self.showTab(tabIndex);
|
|
});
|
|
|
|
if (Helper.isNull(_self.tabHeadingContainer)) {
|
|
_self.tabHeadingContainer = siteContent.querySelector(".tab-header-container");
|
|
}
|
|
_self.tabHeadingContainer.appendChild(newTabHeader);
|
|
return siteContent;
|
|
});
|
|
}
|
|
|
|
showTab(index) {
|
|
if (index >= 0 && index < this.tabKeys.length) {
|
|
this.findBy(".tab-" + this.currentTabIndex).classList.remove("active");
|
|
this.findBy(".tab-" + index).classList.add("active");
|
|
this.tabContainer.removeAllChildren().appendChild(Helper.createLoadingSymbol());
|
|
|
|
this.currentTabIndex = index;
|
|
|
|
const _self = this;
|
|
this.fragments[this.tabKeys[index]].inflatePromise.then(tabView => {
|
|
if (_self.currentTabIndex === index) {
|
|
_self.tabContainer.removeAllChildren().appendChild(tabView);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export { DelayPromise, InstallManager, Matomo, RotateHelper, ScaleHelper, AudioChain, SoundManager, TabbedFragment };
|