OctoSpaccHub/public/FramesBrowser/index.html

568 lines
21 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- TODO:
* options menu/zone?
* js/css injection?
* open file via drag&drop
* windowing system
* reordering tabs
* automatically add useful meta tags to injected HTML data URIs?
* URL hash parameters documentation and more features, also implement extended syntax mode
* investigate bug on Firefox Android with bottom navbar covering part of frame?
* full-screen edit of HTML data URIs? (probably better to not include this and make a dedicated app)
-->
<html lang="en">
<head>
<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</title>
<meta name="description" content="iFrame-based HTML5 Browser for fun and development"/>
<meta property="og:title" content="🪟️ Frames Browser"/>
<meta property="og:description" content="iFrame-based HTML5 Browser for fun and development"/>
<style>
:root {
--BaseMargin: 8px;
--BtnHeight: calc(1rem + var(--BaseMargin));
--BtnActionHeight: calc(2rem + var(--BaseMargin));
--ColorBg: #f0f0f0;
--ColorFg: #0f0f0f;
}
* {
box-sizing: border-box;
}
body {
margin: 0px;
max-width: 100vw;
max-height: 100vh;
}
button {
height: var(--BtnHeight);
}
iframe {
border: none;
width: 100vw;
position: relative;
}
#BoxControls {
overflow: auto;
}
#BoxControls table, #BoxControls table td, #BoxControls table td > * {
height: var(--BtnActionHeight);
min-width: var(--BtnActionHeight);
padding-top: 0;
padding-bottom: 0;
border-spacing: 0;
}
#BtnFullscreen {
position: absolute;
top: 0;
right: 0;
z-index: 1;
margin: 8px;
}
.BoxPopup {
left: 0;
right: 0;
}
.BoxPopup.Container {
position: absolute;
top: 10vh;
z-index: 1;
text-align: center;
}
.BoxPopup.Content {
position: relative;
margin-left: auto;
margin-right: auto;
padding: 8px;
width: fit-content;
max-height: 80vh;
overflow: auto;
box-shadow: gray 4px 4px 4px 0px;
color: var(--ColorFg);
background-color: var(--ColorBg);
}
.BtnAction {
height: var(--BtnActionHeight);
}
div.padded {
padding: 8px;
}
</style>
<script src="../../shared/OctoHub-Global.js"></script>
</head>
<body>
<button id="BtnFullscreen" onclick="ToggleFullscreen()">🎞️ Menu</button>
<div id="BoxControls"><table><tr>
<!--<td><button onclick="ShowAppInfo()"> Info</button></td>-->
<td><button onclick="ToggleDevTools()">📐️ Tools</button></td>
<td>
<button id="BtnFile" onclick="this.nextElementSibling.click()">📄 File</button>
<input type="file" hidden="hidden" style="display: none;" onchange="LoadFile(this.files[0])"/>
</td>
<td><button style="min-width: calc(4em + 4px);" onclick="ZoomFrame()">🔍️ Zoom</button></td>
<td><button onclick="ToggleFullscreen()">🎞️ Hide</button></td>
<td><button style="min-width: calc(4em + 4px);" onclick="ListFrames()">🪟 Tabs</button></td>
<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>
<tr></table></div>
<div id="BoxHandy"></div>
<noscript><div class="NoScript padded"><p>
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><hr/></div></noscript>
<div id="MainAppContent"><div class="padded">
<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>-->
<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>
<!-- TODO <h2>Open-Source, Licensing, Disclaimers</h2> -->
<h2>Usage and Help</h2>
<p>
This app exists because I needed an efficient interface for visualizing specific websites, HTML data in general,
especially via copypaste of data URIs, and allow for even novice users to handle data shared this way.
<br/>
Being itself a webapp, unfortunately it isn't a replacement for any web browser, but that isn't its goal.
This is instead made to be a tool as cross-platform, lightweight, flexible and immediate to use as possible for my scope.
A webpage working with iFrames, that can even function offline, has revealed itself to be the best possible option.
<br/>
In the past I planned to introduce some fancy features like split tabs,
but for now I don't really know yet... stay tuned!!
<br/>
You can read the changelog below, or
<a href="https://octospacc.altervista.org/?s=%23FramesBrowser">follow occasional project updates and stories on my development blog</a>.
</p>
<p>Some tips:</p><ul>
<li>The above menu bar takes minimal vertical space and as much horizontal space as possible. On smaller displays, items get moved outside the screen and can be reached simply via scrolling.</li>
<li>You can open as many new tabs as you want, pointing to whatever URL or uploaded file content, but <a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#web_storage">keep in mind that your browser may limit the amount of storable data</a>.</li>
<li>You can go back to this info page at any moment by clicking "<code>[ Root Window ]</code>" in the "<code>🪟 Tabs</code>" menu.</li>
<li>Clicking "<code>📐️ Tools</code>" will open or close <a href="https://eruda.liriliri.io">the Eruda console</a> as desired. (Note that Eruda is currently only injected in the root window and not inside any iFrame.)</li>
</ul>
<h2>Changelog</h2>
<h3>2024-03-19</h3><ul>
<li>Remove info button from toolbar for now.</li>
<li>Update info page also adding Usage and Help section with tips.</li>
<li>Fixed interference between frame zoom and menu hiding features.</li>
<li>Slight improvements to the UI (buttons and margins).</li>
<li>Optimize storage by automatically removing a saved URL once no open tab points to it.</li>
<!--<li>Make current tab not persist after reload if changed when opened from hash flags.</li>-->
</ul>
<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/#_1|u=https://example.com">specific URI</a> or <a href="https://hub.octt.eu.org/FramesBrowser/#_1|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>
<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>
<script>
Array.from(document.querySelectorAll('a[href]')).forEach(function(el){
el.onclick = function(){ top.location = this.href }
})
</script>
</div></div>
<script>
var AppData, SesAppData, SesAppDataBak;
function SaveAppData(){
localStorage.setItem('org.eu.octt.FramesBrowser.v1', JSON.stringify({ ...AppData, ...(SesAppData.optionsFromUrl ? SesAppDataBak : SesAppData) }));
};
var FrameZoomLevels = [50, 200];
var SampleHtmlContent = `<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>${MainAppContent.innerHTML}</body></html>`;
MainAppContent.innerHTML = '<iframe id="IframeMain"></iframe>';
document.body.style.overflow = 'hidden';
function $new(tag, props){
var el = document.createElement(tag);
if (props) {
Object.keys(props).forEach(function(key){
el[key] = props[key];
});
};
return el;
};
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();
};
function InputHandleKey(ev){
// Enter
if (ev.keyCode == 13) {
LoadFrame();
};
};
function GetTabUrlFromTabIndex (index) {
return (index === -1 ? null : AppData.urls[AppData.tabs[index].urlIndex]);
};
function ShowFrame(index){
var isFrameRoot = (index === -1);
var url = (GetTabUrlFromTabIndex(index) || '');
ListFramesClose();
AppData.currentTabIndex = index;
SaveAppData();
InputUri.disabled = isFrameRoot;
InputUri.value = url;
BtnFile.disabled = isFrameRoot;
BtnLoad.disabled = isFrameRoot;
BtnExcise.disabled = isFrameRoot;
document.querySelector('iframe').src = (isFrameRoot ? `data:text/html;utf8,${encodeURIComponent(SampleHtmlContent)}` : url);
};
function SaveUrl(){
var url = document.querySelector('input[type="text"]').value;
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();
return url;
};
function PruneUnusedUrls () {
var urlsNew = [];
for (var urlIndex in AppData.urls) {
if (AppData.tabs.filter(function(tab){
var usesUrl = (tab.urlIndex === Number(urlIndex));
if (usesUrl) {
tab.urlIndex = urlsNew.length;
}
return usesUrl;
}).length > 0) {
urlsNew.push(AppData.urls[urlIndex]);
}
}
AppData.urls = urlsNew;
SaveAppData();
};
function AddFrame(){
AppData.tabs = AppData.tabs.concat([{}]);
ListFrames();
ShowFrame(AppData.tabs.length - 1);
SaveAppData();
RefreshFramesCounter();
};
function CloseFrame(index){
AppData.tabs.splice(index, 1);
PruneUnusedUrls();
if (AppData.currentTabIndex === index) {
ShowFrame(-1);
} else if (AppData.currentTabIndex > index) {
AppData.currentTabIndex--;
};
ListFrames(); ListFrames();
SaveAppData();
RefreshFramesCounter();
};
function RefreshFramesCounter(){
var count = AppData.tabs.length;
var countHtml = '';
if (count > 99) {
countHtml = '(...)';
} else if (count > 0) {
countHtml = `(${count})`;
};
document.querySelector('button[onclick="ListFrames()"]').textContent = `🪟${countHtml} Tabs`;
};
function GetNeededIframeHeight (hScale=100) {
return (SesAppData.fullscreen ? `${hScale}vh` : `calc(${hScale}vh - (var(--BtnActionHeight) * ${hScale / 100}))`);
}
function ApplyFullscreen () {
if (SesAppData.fullscreen) {
BoxControls.style.display = 'none';
BtnFullscreen.style.display = '';
} else {
BoxControls.style.display = '';
BtnFullscreen.style.display = 'none';
}
ApplyFrameZoom();
}
function ToggleFullscreen () {
SesAppData.fullscreen = !SesAppData.fullscreen;
ApplyFullscreen();
SaveAppData();
}
function LoadFrame(){
var url = document.querySelector('input[type="text"]').value;
if (!url.toLowerCase().startsWith('javascript:')) {
document.querySelector('iframe').src = SaveUrl();
}
};
function ExciseFrame(){
var uri = SaveUrl();
if (uri.toLowerCase().startsWith('data:')) {
opendatauri(uri);
} else {
open(uri, '_blank');
};
};
function LoadFile(file){
var reader = new FileReader();
reader.onload = function(){
document.querySelector('input[type="text"]').value = reader.result;
LoadFrame();
};
reader.readAsDataURL(file);
};
function ApplyFrameZoom () {
var level = FrameZoomLevels[SesAppData.frameZoomIndex];
var levelopp = FrameZoomLevels[FrameZoomLevels.length - 1 - SesAppData.frameZoomIndex];
['bottom', 'top', 'left', 'right', 'scale', 'width'].forEach(function(prop){
IframeMain.style[prop] = '';
});
if (level < 100) {
IframeMain.style.bottom = `calc(${level}vh - 16px)`;
IframeMain.style.right = `${level}vw`;
} else {
IframeMain.style.top = `calc(${levelopp/2}vh - 8px)`;
IframeMain.style.left = `${levelopp/2}vw`;
}
if (SesAppData.frameZoomIndex !== -1) {
IframeMain.style.scale = (level / 100);
IframeMain.style.width = `${levelopp}vw`;
}
IframeMain.style.height = GetNeededIframeHeight(levelopp);
var zoomButton = document.querySelector('button[onclick="ZoomFrame()"]');
if (level < 100) {
zoomButton.textContent = '🔍️(-) Zoom';
} else if (level > 100) {
zoomButton.textContent = '🔍️(+) Zoom';
} else {
zoomButton.textContent = '🔍️ Zoom';
};
}
function ZoomFrame(){
if (SesAppData.frameZoomIndex === FrameZoomLevels.length - 1) {
SesAppData.frameZoomIndex = -1;
} else {
SesAppData.frameZoomIndex ++;
};
ApplyFrameZoom();
SaveAppData();
};
function ListFrames(){
if (!ListFramesClose()){
var Box = NewBoxPopup('BoxFramesList');
var BtnAdd = $new('button', { className: 'BtnAction', innerHTML: ' Add', onclick: AddFrame });
Box.Content.appendChild(BtnAdd);
var BoxList = $new('ul', { style: 'text-align: initial;' });
Box.Content.appendChild(BoxList);
var LiMain = $new('li');
BoxList.appendChild(LiMain);
var BtnMain = $new('button', { innerHTML: '[ Root Window ]', onclick: /*ShowRootFrame*/function(){ ShowFrame(-1) }, disabled: AppData.currentTabIndex === -1 });
LiMain.appendChild(BtnMain);
for (var i=0; i<AppData.tabs.length; i++) {
var li = $new('li');
li.ItemIndex = i;
BoxList.appendChild(li);
var open = $new('button', { innerHTML: `&nbsp;${(GetTabUrlFromTabIndex(i) || '').slice(0, 16)}&nbsp;`, onclick: function(){ShowFrame(this.parentElement.ItemIndex)}, disabled: AppData.currentTabIndex === i });
li.append(open);
var close = $new('button', { innerHTML: '✖️', style: 'float: right;', onclick: function(){CloseFrame(this.parentElement.ItemIndex)} });
li.append(close);
};
};
};
function ListFramesClose(){
var exist = document.querySelector('#BoxFramesList');
if (exist) {
BoxFramesList.remove();
};
return exist;
};
var isDevToolsOpen = false;
function ToggleDevTools(){
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;
}
};
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 };
};
function opendatauri (data) {
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);
} else if (encoding.toLowerCase() === 'utf8') {
data = decodeURIComponent(data);
};
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');
};
// 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('<','&lt;').replaceAll('>','&gt;')}</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() };
window.onload = function(){
Array.from(document.querySelectorAll('noscript, .NoScript')).forEach(function(el){ el.remove() });
AppData = (JSON.parse(localStorage.getItem('org.eu.octt.FramesBrowser.v1')) || {});
SesAppData = {
optionsFromUrl: (AppData.optionsFromUrl || false),
fullscreen: (AppData.fullscreen || false),
frameZoomIndex: (isNaN(parseInt(AppData.frameZoomIndex)) ? -1 : AppData.frameZoomIndex),
};
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);
RefreshFramesCounter();
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],
immersiveView: 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.immersiveView && !SesAppData.fullscreen && ToggleFullscreen();
!flags.immersiveView && SesAppData.fullscreen && ToggleFullscreen();
break;
}
tokens = tokens.slice(1);
}
break;
}
}
}
};
</script>
</body>
</html>