2023-09-21 17:29:40 +02:00
<!DOCTYPE html>
2024-03-19 01:16:47 +01:00
<!-- TODO:
* open URL via url hash
* open app in restricted mode via hash
* options menu/zone?
-->
2023-09-21 17:29:40 +02:00
< html lang = "en" >
< head >
2023-09-23 16:50:18 +02:00
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< meta property = "og:url" content = "https://hub.octt.eu.org/FramesBrowser" / >
< link rel = "canonical" href = "https://hub.octt.eu.org/FramesBrowser" / >
< link rel = "shortcut icon" type = "image/x-icon" href = "../favicon.png" / >
< link rel = "manifest" href = "./manifest.json" / >
< title > 🪟️ Frames Browser (WIP)< / title >
< meta name = "description" content = "iFrame-based HTML5 Browser for fun and development" / >
< meta property = "og:title" content = "🪟️ Frames Browser (WIP)" / >
< meta property = "og:description" content = "iFrame-based HTML5 Browser for fun and development" / >
2023-09-21 17:29:40 +02:00
< style >
2023-09-21 23:09:22 +02:00
:root {
2023-09-23 00:46:50 +02:00
--BaseMargin: 8px;
--BtnHeight: calc(1rem + var(--BaseMargin));
--BtnActionHeight: calc(2rem + var(--BaseMargin));
--ColorBg: #f0f0f0;
--ColorFg: #0f0f0f;
2023-09-21 23:09:22 +02:00
}
2023-09-21 17:29:40 +02:00
* {
box-sizing: border-box;
}
body {
margin: 0px;
2023-09-21 23:09:22 +02:00
max-width: 100vw;
max-height: 100vh;
}
button {
height: var(--BtnHeight);
2023-09-21 17:29:40 +02:00
}
iframe {
border: none;
width: 100vw;
2023-09-22 01:05:42 +02:00
height: calc(100vh - var(--BtnActionHeight));
2023-09-21 17:29:40 +02:00
position: relative;
}
2023-09-23 00:46:50 +02:00
#BoxControls {
overflow: auto;
}
2023-09-22 01:05:42 +02:00
#BoxControls table, #BoxControls table td, #BoxControls table td > * {
2023-09-21 23:09:22 +02:00
height: var(--BtnActionHeight);
min-width: var(--BtnActionHeight);
2023-09-23 00:46:50 +02:00
padding-top: 0;
padding-bottom: 0;
2023-09-21 17:29:40 +02:00
border-spacing: 0;
}
2024-03-19 01:16:47 +01:00
#BtnFullscreen {
position: absolute;
top: 0;
right: 0;
z-index: 1;
margin: 8px;
}
2023-09-21 23:09:22 +02:00
.BoxPopup {
left: 0;
right: 0;
}
2023-09-23 00:46:50 +02:00
.BoxPopup.Container {
position: absolute;
top: 10vh;
z-index: 1;
text-align: center;
}
.BoxPopup.Content {
position: relative;
margin-left: auto;
margin-right: auto;
width: fit-content;
2023-09-23 16:50:18 +02:00
max-height: 80vh;
overflow: auto;
2023-09-23 00:46:50 +02:00
box-shadow: gray 4px 4px 4px 0px;
color: var(--ColorFg);
background-color: var(--ColorBg);
}
.BtnAction {
height: var(--BtnActionHeight);
}
2023-09-21 17:29:40 +02:00
< / style >
< script >
var FrameZoomLevels = [50, 200];
2024-03-15 22:04:56 +01:00
var AppInfoString = `Frames Browser v2024-03-15.`;
2023-09-21 17:29:40 +02:00
< / script >
< / head >
< body >
2024-03-19 01:16:47 +01:00
< button id = "BtnFullscreen" onclick = "ToggleFullscreen()" > 🎞️ Menu< / button >
2023-09-21 17:29:40 +02:00
< div id = "BoxControls" > < table > < tr >
< td > < button onclick = "ShowAppInfo()" > ℹ ️ Info< / button > < / td >
2023-09-23 16:50:18 +02:00
< td > < button onclick = "ToggleDevTools()" > 📐️ Tools< / button > < / td >
2023-09-22 01:05:42 +02:00
< td >
2023-09-23 16:50:18 +02:00
< button id = "BtnFile" onclick = "this.nextElementSibling.click()" > 📄 File< / button >
2023-09-22 01:05:42 +02:00
< input type = "file" hidden = "hidden" style = "display: none;" onchange = "LoadFile(this.files[0])" / >
< / td >
2023-09-21 17:29:40 +02:00
< td > < button onclick = "ZoomFrame()" > 🔍️ Zoom< / button > < / td >
2024-03-19 01:16:47 +01:00
< td > < button onclick = "ToggleFullscreen()" > 🎞️ Hide< / button > < / td >
2023-09-23 00:46:50 +02:00
< td > < button onclick = "ListFrames()" > 🪟 Frames< / button > < / td >
2023-09-23 16:50:18 +02:00
< td style = "width: 100%;" > < input id = "InputUri" type = "text" style = "min-width: 100%;" placeholder = "🔗️ Enter URI..." onkeydown = "InputHandleKey(event)" / > < / td >
< td > < button id = "BtnLoad" onclick = "LoadFrame()" > ↩️ Load< / button > < / td >
< td > < button id = "BtnExcise" onclick = "ExciseFrame()" > ↗️ Excise< / button > < / td >
2023-09-21 17:29:40 +02:00
< tr > < / table > < / div >
2023-09-23 00:46:50 +02:00
< noscript > < p class = "NoScript" >
This is an actual app, not a badly-made website.
< br >
It needs JavaScript to work, so you need to enable it.
< br >
The code is fully open, and you can review it with "View Page Source".
< / p > < / noscript >
< div id = "BoxHandy" > < / div >
2024-03-15 22:04:56 +01:00
< div id = "MainAppContent" >
< h1 > Frames Browser< / h1 >
< p > Frames Browser is an iFrame-based HTML5 browser made for fun and development. Use the above menu to operate the app.< / p >
< p > Note: the app is still in development and data handling may break between versions! Backup your data externally to avoid losing it.< / p >
2024-03-19 01:16:47 +01:00
< p > You can also visit, or go back to, my home page (OctoSpacc Hub): < a href = "https://hub.octt.eu.org" > hub.octt.eu.org< / a > !< / p >
2024-03-15 22:04:56 +01:00
< h2 > Changelog< / h2 >
2024-03-19 01:16:47 +01:00
< h3 > 2024-03-18< / h3 > < ul >
< li > First implementation of handling of URL hash parameters (reduced or "quick" format).< / li >
< li > Open a < a href = "https://hub.octt.eu.org/FramesBrowser/#_0|u=https://example.com" > specific URI< / a > or < a href = "https://hub.octt.eu.org/FramesBrowser/#_0|h=<h2>Hello World!</h2>" > raw HTML data< / a > via URL hash (click the links to try!).< / li >
< li > Add "fullscreen" (hide the main app bar) option to GUI (persistent) and quick URL hash flags.< / li >
< li > Fixed some issues with handling UTF8 data URIs.< / li >
< li > Make frame zoom option persistent.< / li >
< / ul >
< h3 > 2024-03-17< / h3 > < ul >
< li > Improve internal app state data handling (breaking previous version).< / li >
< li > Add data dump and reset options appearing on boot to users with pre-update database.< / li >
< li > Add link back to my hub in this README.< / li >
< / ul >
2024-03-15 22:04:56 +01:00
< h3 > 2024-03-15< / h3 > < ul >
< li > Improve some crusty code.< / li >
< li > Add this default/sample HTML text into the app, visible via the otherwise useless Root Window.< / li >
< li > Add symbol for indicating current zoom level on zoom button.< / li >
< li > Add counter for open frames (goes up to 99 before becoming generic).< / li >
< li > Better handling of devtools, removed floating button and always use dedicated button.< / li >
< / ul >
< h3 > 2023-09-23 and before< / h3 > < ul >
< li > MVP version of the app with finished UI and basic features.< / li >
< li > Eruda DevTools.< / li >
< li > In-frame page zoom with 3 levels.< / li >
< li > Creating, deleting, and switching to different frames, inputting URL.< / li >
< li > Persistent app state saving frames and their URLs.< / li >
< li > Excise frame content into another native browser tab (via opening the same URL)< / li >
< li > Import file into data URI from local file.< / li >
< / ul >
2024-03-19 01:16:47 +01:00
< script >
Array.from(document.querySelectorAll('a[href]')).forEach(function(el){
el.onclick = function(){ top.location = this.href }
})
< / script >
2024-03-15 22:04:56 +01:00
< / div >
2023-09-21 17:29:40 +02:00
< script >
2024-03-19 01:16:47 +01:00
var AppData, SesAppData, SesAppDataBak;
function SaveAppData(){
localStorage.setItem('org.eu.octt.FramesBrowser.v1', JSON.stringify({ ...AppData, ...(SesAppData.optionsFromUrl ? SesAppDataBak : SesAppData) }));
};
2024-03-15 22:04:56 +01:00
var SampleHtmlContent = MainAppContent.innerHTML;
MainAppContent.innerHTML = '< iframe > < / iframe > ';
document.body.style.overflow = 'hidden';
2023-09-21 17:29:40 +02:00
2023-09-23 00:46:50 +02:00
function $new(tag, props){
var el = document.createElement(tag);
if (props) {
Object.keys(props).forEach(function(key){
el[key] = props[key];
});
};
return el;
2023-09-22 01:05:42 +02:00
};
2023-09-21 23:09:22 +02:00
2023-09-23 16:50:18 +02:00
function $request(url, opts){
if (!opts.method) {
opts.method = 'GET';
};
var req = new XMLHttpRequest();
req.onreadystatechange = function(){
if (this.readyState == 4) {
opts.callback(req.responseText);
};
};
req.open(opts.method, url, true);
req.send();
};
2023-09-21 17:29:40 +02:00
function ShowAppInfo(){
alert(AppInfoString);
};
2023-09-23 16:50:18 +02:00
function InputHandleKey(ev){
// Enter
if (ev.keyCode == 13) {
LoadFrame();
};
};
2024-03-19 01:16:47 +01:00
function GetTabUrlFromTabIndex (index) {
return (index === -1 ? null : AppData.urls[AppData.tabs[index].urlIndex]);
};
2023-09-23 00:46:50 +02:00
function ShowFrame(index){
2024-03-15 22:04:56 +01:00
var isFrameRoot = (index === -1);
2024-03-19 01:16:47 +01:00
var url = (GetTabUrlFromTabIndex(index) || '');
2023-09-23 00:46:50 +02:00
ListFramesClose();
2024-03-19 01:16:47 +01:00
AppData.currentTabIndex = index;
SaveAppData();
2024-03-15 22:04:56 +01:00
InputUri.disabled = isFrameRoot;
2024-03-19 01:16:47 +01:00
InputUri.value = url;
2024-03-15 22:04:56 +01:00
BtnFile.disabled = isFrameRoot;
BtnLoad.disabled = isFrameRoot;
BtnExcise.disabled = isFrameRoot;
2024-03-19 01:16:47 +01:00
document.querySelector('iframe').src = (isFrameRoot ? `data:text/html;utf8,${encodeURIComponent(SampleHtmlContent)}` : url);
2023-09-23 00:46:50 +02:00
};
2023-09-21 23:09:22 +02:00
function SaveUrl(){
2023-09-22 01:05:42 +02:00
var url = document.querySelector('input[type="text"]').value;
2024-03-19 01:16:47 +01:00
var urlIndex = AppData.urls.indexOf(url);
if (urlIndex === -1) {
// it's a new url, store it
AppData.urls.push(url);
urlIndex = (AppData.urls.length - 1);
}
AppData.tabs[AppData.currentTabIndex].urlIndex = urlIndex;
PruneUnusedUrls();
SaveAppData();
2023-09-21 23:09:22 +02:00
return url;
2023-09-23 00:46:50 +02:00
};
2024-03-19 01:16:47 +01:00
function PruneUnusedUrls(){
// TODO
};
2023-09-23 00:46:50 +02:00
function AddFrame(){
2024-03-19 01:16:47 +01:00
AppData.tabs = AppData.tabs.concat([{}]);
2024-03-15 22:04:56 +01:00
ListFrames();
2024-03-19 01:16:47 +01:00
ShowFrame(AppData.tabs.length - 1);
SaveAppData();
2024-03-15 22:04:56 +01:00
RefreshFramesCounter();
2023-09-23 00:46:50 +02:00
};
function CloseFrame(index){
2024-03-19 01:16:47 +01:00
AppData.tabs.splice(index, 1);
PruneUnusedUrls();
if (AppData.currentTabIndex === index) {
ShowFrame(-1);
} else if (AppData.currentTabIndex > index) {
AppData.currentTabIndex--;
2023-09-23 00:46:50 +02:00
};
ListFrames(); ListFrames();
2024-03-19 01:16:47 +01:00
SaveAppData();
2024-03-15 22:04:56 +01:00
RefreshFramesCounter();
2023-09-23 00:46:50 +02:00
};
2024-03-15 22:04:56 +01:00
function RefreshFramesCounter(){
2024-03-19 01:16:47 +01:00
var count = AppData.tabs.length;
2024-03-15 22:04:56 +01:00
var countHtml = '';
if (count > 99) {
countHtml = '(...)';
} else if (count > 0) {
countHtml = `(${count})`;
};
document.querySelector('button[onclick="ListFrames()"]').textContent = `🪟${countHtml} Frames`;
};
2024-03-19 01:16:47 +01:00
function ApplyFullscreen () {
if (SesAppData.fullscreen) {
BoxControls.style.display = 'none';
BtnFullscreen.style.display = '';
} else {
BoxControls.style.display = '';
BtnFullscreen.style.display = 'none';
}
}
function ToggleFullscreen () {
SesAppData.fullscreen = !SesAppData.fullscreen;
ApplyFullscreen();
SaveAppData();
}
2023-09-21 23:09:22 +02:00
function LoadFrame(){
document.querySelector('iframe').src = SaveUrl();
};
function ExciseFrame(){
2023-09-23 00:46:50 +02:00
var uri = SaveUrl();
if (uri.toLowerCase().startsWith('data:')) {
opendatauri(uri);
} else {
open(uri, '_blank');
2023-09-22 01:05:42 +02:00
};
2023-09-21 23:09:22 +02:00
};
2023-09-22 01:05:42 +02:00
function LoadFile(file){
var reader = new FileReader();
reader.onload = function(){
2023-09-23 16:50:18 +02:00
document.querySelector('input[type="text"]').value = reader.result;
2023-09-22 01:05:42 +02:00
LoadFrame();
};
2023-09-23 16:50:18 +02:00
reader.readAsDataURL(file);
2023-09-21 17:29:40 +02:00
};
2024-03-19 01:16:47 +01:00
function ApplyFrameZoom () {
var level = FrameZoomLevels[SesAppData.frameZoomIndex];
var levelopp = FrameZoomLevels[FrameZoomLevels.length - 1 - SesAppData.frameZoomIndex];
2023-09-21 17:29:40 +02:00
var stylepos = (level < 100
? `right: ${level}vw; bottom: calc(${level}vh - 16px);`
: `left: ${levelopp/2}vw; top: calc(${levelopp/2}vh - 8px);`
);
2024-03-19 01:16:47 +01:00
document.querySelector('iframe').style = (SesAppData.frameZoomIndex === -1
2023-09-21 17:29:40 +02:00
? ''
2023-09-22 01:05:42 +02:00
: `scale: ${level/100}; width: ${levelopp}vw; height: calc(${levelopp}vh - (var(--BtnActionHeight) * ${levelopp / 100})); ${stylepos}`
2023-09-21 17:29:40 +02:00
);
2024-03-15 22:04:56 +01:00
var zoomButton = document.querySelector('button[onclick="ZoomFrame()"]');
if (level < 100 ) {
zoomButton.textContent = '🔍️(-) Zoom';
} else if (level > 100) {
zoomButton.textContent = '🔍️(+) Zoom';
} else {
zoomButton.textContent = '🔍️ Zoom';
};
2024-03-19 01:16:47 +01:00
}
function ZoomFrame(){
if (SesAppData.frameZoomIndex === FrameZoomLevels.length - 1) {
SesAppData.frameZoomIndex = -1;
} else {
SesAppData.frameZoomIndex ++;
};
ApplyFrameZoom();
SaveAppData();
2023-09-21 23:09:22 +02:00
};
function ListFrames(){
2023-09-23 00:46:50 +02:00
if (!ListFramesClose()){
var Box = NewBoxPopup('BoxFramesList');
var BtnAdd = $new('button', { className: 'BtnAction', innerHTML: '➕ Add', onclick: AddFrame });
Box.Content.appendChild(BtnAdd);
var BoxList = $new('ul');
Box.Content.appendChild(BoxList);
var LiMain = $new('li');
BoxList.appendChild(LiMain);
2024-03-19 01:16:47 +01:00
var BtnMain = $new('button', { innerHTML: 'Root Window', onclick: /*ShowRootFrame*/function(){ ShowFrame(-1) }, disabled: AppData.currentTabIndex === -1 });
2023-09-23 00:46:50 +02:00
LiMain.appendChild(BtnMain);
2024-03-19 01:16:47 +01:00
for (var i=0; i< AppData.tabs.length ; i + + ) {
2023-09-23 00:46:50 +02:00
var li = $new('li');
li.ItemIndex = i;
BoxList.appendChild(li);
2024-03-19 01:16:47 +01:00
var open = $new('button', { innerHTML: ` ${(GetTabUrlFromTabIndex(i) || '').slice(0, 16)} `, onclick: function(){ShowFrame(this.parentElement.ItemIndex)}, disabled: AppData.currentTabIndex === i });
2023-09-23 00:46:50 +02:00
li.append(open);
var close = $new('button', { innerHTML: '✖️', onclick: function(){CloseFrame(this.parentElement.ItemIndex)} });
li.append(close);
};
};
};
function ListFramesClose(){
var exist = document.querySelector('#BoxFramesList');
if (exist) {
BoxFramesList.remove();
};
return exist;
};
2024-03-15 22:04:56 +01:00
var isDevToolsOpen = false;
2023-09-23 16:50:18 +02:00
function ToggleDevTools(){
2024-03-15 22:04:56 +01:00
if (typeof(eruda) === 'undefined') {
$request('https://cdn.jsdelivr.net/npm/eruda', { callback: function(text){
eval(text);
eruda.init();
eruda._shadowRoot.querySelector('div.eruda-entry-btn').style.display = 'none';
eruda.show();
isDevToolsOpen = true;
} });
} else {
eruda[isDevToolsOpen ? 'hide' : 'show']();
isDevToolsOpen = !isDevToolsOpen;
}
2023-09-23 16:50:18 +02:00
};
2023-09-23 00:46:50 +02:00
function NewBoxPopup(id){
var Container = $new('div', { id: id, className: 'BoxPopup Container' });
var Content = $new('div', { className: 'BoxPopup Content' });
Container.appendChild(Content);
var BtnClose = $new('button', { className: 'BtnAction', innerHTML: '❌ Close', onclick: function(){this.parentElement.parentElement.remove()} });
Content.appendChild(BtnClose);
BoxHandy.appendChild(Container);
return { Container: Container, Content: Content };
};
2024-03-19 01:16:47 +01:00
function opendatauri (data) {
2023-09-23 00:46:50 +02:00
var head = data.split(',')[0].split('data:')[1];
var [mime, encoding] = head.split(';');
data = data.split(',').slice(1).join(',');
if (encoding.toLowerCase() === 'base64') {
data = atob(data);
2024-03-19 01:16:47 +01:00
} else if (encoding.toLowerCase() === 'utf8') {
data = decodeURIComponent(data);
2023-09-23 00:46:50 +02:00
};
var bytes = new Array(data.length);
for (var i = 0; i < data.length ; i + + ) {
bytes[i] = data.charCodeAt(i);
};
window.open(URL.createObjectURL(
new Blob([new Uint8Array(bytes)], { type: `${mime};${encoding ? encoding : 'utf8'}` })
), '_blank');
2023-09-21 17:29:40 +02:00
};
2024-03-19 01:16:47 +01:00
// https://stackoverflow.com/questions/45053624/convert-hex-to-binary-in-javascript/45054052#45054052
function hex2bin (hex) {
return (parseInt(hex, 16).toString(2)).padStart((hex.length * 4), '0');
}
function AlertMigrateAppData(){
var stored = Object.keys(localStorage);
if (stored.includes('FramesBrowser.CurrentFrames') || stored.includes('FramesBrowser.FrameIndexes') || stored.includes('FramesBrowser.url')) {
var overlay = document.createElement('div');
overlay.style = 'position: absolute; width: 100vw; height: 100vh; top: 0; background: white; color: black;';
overlay.innerHTML = `< p > The app handling of data has changed in the last update. Please copy and backup externally all the data you need and reset the app to continue using it.< / p >
< button > Reset< / button >
< p > Your open URLs:< / p > < ul > ${JSON.parse(localStorage.getItem('FramesBrowser.CurrentFrames'))?.map(function(item){
return `< li > ${item.replaceAll('< ','< ').replaceAll('>','> ')}< / li > `;
}).join('') || '< p > [nothing previously saved]< / p > '}< / ul > `;
overlay.querySelector('button').onclick = function(){
['CurrentFrames','FrameIndexes','url'].forEach(function(key){
localStorage.removeItem(`FramesBrowser.${key}`);
});
location.reload();
};
document.body.appendChild(overlay);
}
};
window.onhashchange = function(){ location.reload() };
2024-03-15 22:04:56 +01:00
window.onload = function(){
2023-09-23 00:46:50 +02:00
Array.from(document.querySelectorAll('noscript, .NoScript')).forEach(function(el){ el.remove() });
2024-03-19 01:16:47 +01:00
AppData = (JSON.parse(localStorage.getItem('org.eu.octt.FramesBrowser.v1')) || {});
SesAppData = {
fullscreen: (AppData.fullscreen || false),
frameZoomIndex: (isNaN(parseInt(AppData.frameZoomIndex)) ? -1 : AppData.frameZoomIndex),
optionsFromUrl: (AppData.optionsFromUrl || false),
};
SesAppDataBak = structuredClone(SesAppData);
AppData = {
tabs: (AppData.tabs || []),
urls: (AppData.urls || []),
currentTabIndex: (isNaN(parseInt(AppData.currentTabIndex)) ? -1 : AppData.currentTabIndex),
};
AlertMigrateAppData();
SaveAppData();
document.querySelector('input[type="text"]').value = GetTabUrlFromTabIndex(AppData.currentTabIndex);
ApplyFullscreen();
ApplyFrameZoom();
ShowFrame(AppData.currentTabIndex);
2024-03-15 22:04:56 +01:00
RefreshFramesCounter();
2024-03-19 01:16:47 +01:00
if (location.hash) {
var tokens = location.hash.slice(1).split('|');
if (tokens[0].startsWith('_')) {
// leading underscore indicates a restricted quick query, succeding charaters are identifiers for versioning
var paramsVersion = tokens[0].slice(1);
var flags = {};
SesAppData.optionsFromUrl = true;
tokens = tokens.slice(1);
switch (paramsVersion) {
case '1':
while (tokens.length > 0) {
var opt = tokens[0];
var optLow = opt.toLowerCase();
if (optLow.startsWith('f=')) {
var flagStr = hex2bin(opt.slice(2));
var flags = {
//disallowPrefsOverride: flagStr[0],
fullscreen: flagStr[3],
};
for (var flag in flags) {
flags[flag] = !!Number(flags[flag]);
}
} else if (optLow.startsWith('u=') || optLow.startsWith('h=')) {
// load data URI, or HTML content
tokens[0] = tokens[0].slice(2);
var fieldData = tokens.join('|');
var url = ((optLow.startsWith('h=') ? 'data:text/html;utf8,' : '') + fieldData);
if (GetTabUrlFromTabIndex(AppData.currentTabIndex) !== url) {
AddFrame();
document.querySelector('input[type="text"]').value = url;
LoadFrame();
}
flags.fullscreen & & !SesAppData.fullscreen & & ToggleFullscreen();
!flags.fullscreen & & SesAppData.fullscreen & & ToggleFullscreen();
break;
}
tokens = tokens.slice(1);
}
break;
}
}
}
2023-09-21 17:29:40 +02:00
};
< / script >
< / body >
< / html >