Skip to content

Instantly share code, notes, and snippets.

@dbarjs
Last active September 9, 2023 16:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbarjs/9834c4505cd217ebd230f1ebaa9a68d7 to your computer and use it in GitHub Desktop.
Save dbarjs/9834c4505cd217ebd230f1ebaa9a68d7 to your computer and use it in GitHub Desktop.
Framerize.js
(function () {
'use strict';
/**
* @typedef {'warn' | 'info' | 'log' | 'error'} LoggerType
*/
/**
* @typedef {Object} UseSoundAlertOptions
* @property {number} timeBetweenPlays
* @property {string} defaultSoundId
* @property {boolean} isEnabled
*/
/**
* @typedef {'loading' | 'success' | 'error'} FrameStatus
*/
/**
* @typedef {Object} FrameOptions
* @property {number} autoReloadDelay
* @property {number} timeoutDelay
* @property {number} reloadTimeAfterTimeout
*/
/**
* @typedef {Object} FrameLogObject
* @property {number} key
* @property {string} url
* @property {FrameStatus} status
* @property {boolean} isMounted
*/
/**
* Integration with the browser's `console`
* @param {LoggerType} type
* @param {...any} args
*/
function logger(type, ...args) {
const prefix = '[framerize.js]';
// eslint-disable-next-line no-console
const log = console[type] || console.log;
if (typeof log !== 'function') {
return;
}
log(prefix, ...args);
}
/**
* @param {UseSoundAlertOptions} options
*/
function useSoundAlert(options = {}) {
let isEnabled = !!options.isEnabled;
/**
* @link https://notificationsounds.com/alarm-sounds-alerts-ringtones
*/
const avaiableSounds = {
direct:
'https://firebasestorage.googleapis.com/v0/b/flux-jobboard.appspot.com/o/direct-545.ogg?alt=media&token=78d73b30-c4ba-4b8c-aeb7-c65413e47ed8',
'gentle-alarm':
'https://firebasestorage.googleapis.com/v0/b/flux-jobboard.appspot.com/o/gentle-alarm-474.ogg?alt=media&token=eba4a701-d5ee-42b6-817e-0e73e2e755c6',
'little-bell':
'https://firebasestorage.googleapis.com/v0/b/flux-jobboard.appspot.com/o/little-bell-430.ogg?alt=media&token=43a4036b-7faa-41a2-a62d-f19aeefd7810',
'piece-of-cake':
'https://firebasestorage.googleapis.com/v0/b/flux-jobboard.appspot.com/o/piece-of-cake-611.ogg?alt=media&token=26a6b67d-5729-4d85-8ee8-dce48557bbce',
whistling:
'https://firebasestorage.googleapis.com/v0/b/flux-jobboard.appspot.com/o/whistling-ringtone.ogg?alt=media&token=c72b9165-82b1-4332-ac88-5ee94cced9b8',
};
const defaultSoundId = options.defaultSoundId || 'gentle-alarm';
const playlist = {
queue: [],
timeBetweenPlays: options.timeBetweenPlays || 0,
debounceTimer: null,
isPlaying: false,
};
const isDebounceEnabled = !!playlist.timeBetweenPlays;
const playNext = () => {
if (!playlist.queue.length) {
return;
}
if (playlist.debounceTimer) {
return;
}
const nextSoundId = playlist.queue.shift();
if (isDebounceEnabled) {
playlist.debounceTimer = setTimeout(() => {
play(nextSoundId);
}, playlist.timeBetweenPlays);
} else {
play(nextSoundId);
}
};
const addToPlaylist = (soundId) => {
playlist.queue.push(soundId);
};
/**
* @param {string} soundId
* @param {boolean} ignoreDebounce
*/
const play = async (soundId = defaultSoundId) => {
if (!isEnabled) {
return;
}
const soundUrl = avaiableSounds[soundId];
if (!soundUrl) {
logger('warn', 'sound not found', soundId);
return;
}
if (playlist.isPlaying) {
return addToPlaylist(soundId);
}
playlist.isPlaying = true;
try {
const audio = new Audio(soundUrl);
await audio.play();
} catch (error) {
logger('error', 'error playing sound', soundId, error);
}
playlist.isPlaying = false;
playNext();
};
const enable = () => {
isEnabled = true;
};
const disable = () => {
isEnabled = false;
};
/**
* @returns {HTMLElement} element
*/
const createToggleButton = () => {
const element = document.createElement('button');
element.innerText = isEnabled ? 'mute sounds' : 'play sounds';
element.addEventListener('click', () => {
if (isEnabled) {
disable();
element.innerText = 'play sounds';
} else {
enable();
element.innerText = 'mute sounds';
play('direct');
}
});
return element;
};
return {
play,
enable,
disable,
createToggleButton,
};
}
/**
* Integration with the browser's Notification API
*
* @param {NotificationOptions} options
*/
function useNotification(options) {
/**
* @type {NotificationOptions} defaultOptions
*/
const defaultOptions = {
// vibrate: [200, 100, 200],
...options,
};
let hasPermission = false;
/**
* @param {boolean | undefined} forceRequest
*/
const checkPermission = async (forceRequest) => {
if (hasPermission) {
return;
}
if (!('Notification' in window)) {
logger('error', 'This browser does not support desktop notification');
return;
}
if (!forceRequest && Notification.permission === 'denied') {
return;
}
if (Notification.permission === 'granted') {
hasPermission = true;
return;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
hasPermission = true;
}
logger('debug', 'permission granted?', permission, hasPermission);
};
/**
*
* @param {string} title
* @param {string} body
*/
const push = async (title, body) => {
logger('debug', 'pushing notification', title, body);
const create = () => {
const notification = new Notification(title, {
...defaultOptions,
body,
});
return notification;
};
await checkPermission();
if (!hasPermission) {
logger('warn', 'no permission to send notifications');
return;
}
logger('debug', 'notification:', title, body);
create(title, body);
};
return {
checkPermission,
push,
};
}
const {
push: pushNotification,
checkPermission: checkNotificationPermission,
} = useNotification();
const { play: playSoundAlert, createToggleButton: createSoundAlertButton } =
useSoundAlert();
class Frame {
/**
* @type {number} key
*/
key = 0;
/**
* @type {HTMLElement | null} containerElement
*/
#containerElement = null;
/**
* @type {HTMLElement | null} statusElement
*/
#statusElement = null;
/**
* @type {HTMLElement | null} iframeElement
*/
#iframeElement = null;
/**
* @type {FrameStatus} status
*/
#status = 'loading';
/**
* @type {string} url
*/
#url = '';
/**
* @type {FrameOptions} options
*/
#options = {
autoReloadDelay: 60 * 1000,
timeoutDelay: 20 * 1000,
reloadTimeAfterTimeout: 5 * 1000,
};
/**
* @type {boolean} isMounted
*/
#isMounted = false;
/**
* @type {any} interval
*/
#autoReloadTimer = null;
/**
* @type {any} interval
*/
#timeoutTimer = null;
/**
* @param {string} url
* @param {number} key
* @param {FrameOptions} options
*/
constructor(url, key, options) {
this.#url = url;
this.key = key;
if (options) {
this.#options = {
...this.#options,
...options,
};
}
this.init();
}
/**
* @return {HTMLElement | null} container
*/
get element() {
return this.#containerElement;
}
/**
* @return {HTMLElement} container
*/
#createContainer() {
const element = document.createElement('article');
element.style.position = 'relative';
element.style.minWidth = '50vw';
element.style.display = 'flex';
element.style.flexDirection = 'column';
element.style.justifyContent = 'center';
element.style.alignItems = 'center';
element.style.boxSizing = 'border-box';
return element;
}
/**
* @return {HTMLElement} statusText
*/
#createStatusElement() {
const element = document.createElement('div');
element.style.position = 'absolute';
element.style.top = '0';
element.style.left = '0';
return element;
}
/**
* @return {HTMLElement} iframe
*/
#createIframe() {
const element = document.createElement('iframe');
element.style.width = '100%';
element.style.height = '100%';
element.style.border = 'none';
element.style.boxSizing = 'border-box';
return element;
}
/**
* @returns {void}
*/
#addEventListeners() {
this.#iframeElement.addEventListener('load', () => {
this.#setStatus('success');
});
this.#iframeElement.addEventListener('error', () => {
this.#setStatus('error', 'Iframe load error!');
pushNotification('[framerize.js] Error!', 'Error loading iframe');
playSoundAlert('gentle-alarm');
});
}
/**
* @returns {void}
*/
#mount() {
logger('debug', `mounting iframe #${this.key}`);
if (!this.#containerElement) {
this.#containerElement = this.#createContainer();
}
if (!this.#statusElement) {
this.#statusElement = this.#createStatusElement();
this.#containerElement.appendChild(this.#statusElement);
}
this.#iframeElement = this.#createIframe();
this.#containerElement.appendChild(this.#iframeElement);
this.#isMounted = true;
}
/**
* @returns {void}
*/
#initIframe() {
logger('debug', `initializing iframe #${this.key}`);
this.#iframeElement.src = this.#url;
}
/**
* @param {number} customDelay
*/
#runAutoReload(customDelay) {
this.reloadTimeout = setTimeout(() => {
this.reload();
}, customDelay || this.#options.autoReloadDelay);
}
/**
* @returns {void}
*/
#onTimeout() {
if (this.#status === 'success') {
return;
}
this.#setStatus('error', 'timeout error!');
logger('error', `timeout error #${this.key}`, this.toLogObject());
playSoundAlert('little-bell');
pushNotification('[framerize.js] Timeout!', 'Timeout loading iframe');
this.destroy(true);
this.#runAutoReload(this.#options.reloadTimeAfterTimeout);
}
/**
* @returns {void}
*/
#runTimeout() {
this.#timeoutTimer = setTimeout(() => {
this.#onTimeout();
}, this.#options.timeoutDelay);
}
/**
* @param {FrameStatus} status
* @param {string | undefined} customMessage
* @return {void}
*/
#setStatus(status, customMessage) {
this.#status = status;
const updateStatusElement = (backgroundColor, message) => {
this.#statusElement.style.backgroundColor = backgroundColor;
this.#statusElement.innerText = customMessage || message;
};
switch (this.#status) {
case 'loading': {
updateStatusElement('rgba(0, 0, 255, 0.5)', 'loading...');
break;
}
case 'success': {
updateStatusElement('rgba(0, 255, 0, 0.5)', 'success!');
break;
}
case 'error': {
updateStatusElement('rgba(255, 0, 0, 0.5)', 'error!');
break;
}
default: {
updateStatusElement('rgba(0, 0, 0, 0.5)', '???');
}
}
}
/**
* @param {boolean} keepContainer
* @returns {void}
*/
destroy(keepContainer = false) {
if (!this.#autoReloadTimer) {
clearTimeout(this.#autoReloadTimer);
}
if (this.#timeoutTimer) {
clearTimeout(this.#timeoutTimer);
}
if (this.#iframeElement) {
this.#containerElement.removeChild(this.#iframeElement);
this.#iframeElement = null;
}
if (!keepContainer) {
this.#statusElement.remove();
this.#statusElement = null;
this.#containerElement.remove();
this.#containerElement = null;
}
this.#isMounted = false;
}
/**
* @returns {void}
*/
reload() {
logger('info', `reloading iframe #${this.key}`, this.toLogObject());
try {
this.destroy(true);
} catch (error) {
this.#setStatus('error');
playSoundAlert('whistling');
logger(
'error',
`error reloading iframe #${this.key}`,
this.toLogObject(),
error,
);
}
this.init();
}
/**
* @returns {void}
*/
init() {
logger('info', `initializing iframe #${this.key}`);
if (!this.#isMounted) {
this.#mount();
}
this.#setStatus('loading');
this.#addEventListeners();
this.#initIframe();
this.#runAutoReload();
this.#runTimeout();
}
/**
* @returns {FrameLogObject} logObject
*/
toLogObject() {
const logObject = {
key: this.key,
url: this.#url,
status: this.#status,
isMounted: this.#isMounted,
};
return JSON.parse(JSON.stringify(logObject));
}
}
class Framerize {
/**
* @param {Array<String>} urls
*/
urls = [];
/**
* @type {HTMLElement} dom
*/
dom = null;
/**
* @type {HTMLElement} layout
*/
layout = null;
/**
* @type {Array<Frame>} frames
*/
frames = [];
/**
* @type {FrameOptions | undefined} frameOptions
*/
frameOptions;
/**
* @param {Array<string>} urls
*/
constructor(urls, frameOptions) {
this.urls = urls;
this.frameOptions = frameOptions;
}
/**
* @return {HTMLElement} layout
*/
createLayout() {
const layout = document.createElement('main');
layout.style.margin = '0';
layout.style.padding = '0';
layout.style.position = 'relative';
layout.style.width = '100%';
layout.style.height = '100%';
layout.style.display = 'flex';
layout.style.flexDirection = 'row';
layout.style.flexWrap = 'wrap';
layout.style.justifyContent = 'flex-start';
layout.style.alignItems = 'stretch';
layout.style.boxSizing = 'border-box';
const toggleButton = createSoundAlertButton();
toggleButton.style.position = 'fixed';
toggleButton.style.top = '0';
toggleButton.style.right = '0';
toggleButton.style.zIndex = '9999';
layout.appendChild(toggleButton);
return layout;
}
/**
* @return {HTMLElement} dom
*/
createDom() {
const createHead = () => {
const head = window.document.createElement('head');
const title = window.document.createElement('title');
title.innerText = 'Framerize';
const meta = window.document.createElement('meta');
meta.setAttribute('charset', 'utf-8');
return head;
};
const createBody = () => {
const body = window.document.createElement('body');
body.style.margin = '0';
body.style.padding = '0';
body.style.width = '100vw';
body.style.height = '100vh';
body.style.overflow = 'hidden';
body.style.display = 'flex';
body.style.flexDirection = 'column';
body.style.justifyContent = 'center';
body.style.fontFamily = 'Ubuntu Mono, monospace';
body.style.alignItems = 'center';
body.style.boxSizing = 'border-box';
return body;
};
const createHtml = () => {
const html = window.document.createElement('html');
return html;
};
const html = createHtml();
const mount = () => {
html.appendChild(createHead());
html.appendChild(createBody());
};
mount();
return html;
}
/**
* @param {String} url
* @param {number} key
* @return {Frame} frame
*/
createFrame(url, key) {
const frame = new Frame(url, key);
return frame;
}
/**
* @return {void}
*/
createFrames() {
return this.urls.map(this.createFrame);
}
/**
* @returns {void}
*/
destroyDom() {
window.document.removeChild(window.document.documentElement);
}
/**
* @returns {void}
*/
#mount() {
this.destroyDom();
this.dom = this.createDom();
window.document.appendChild(this.dom);
this.layout = this.createLayout();
window.document.body.appendChild(this.layout);
this.frames = this.createFrames();
this.frames.forEach((frame) => {
this.layout.appendChild(frame.element);
});
}
/**
* @returns {void}
*/
async init() {
this.#mount();
this.frames.forEach((frame) => {
frame.init();
});
checkNotificationPermission(true);
}
}
})();
const framerize = new Framerize(
[
'https://google.com',
'https://google.com',
'https://google.com',
'https://google.com',
'https://google.com',
'https://google.com',
'https://google.com',
],
{
autoReloadDelay: 120 * 1000,
timeoutDelay: 30 * 1000,
reloadTimeAfterTimeout: 10 * 1000,
},
);
framerize.init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment