356 lines
12 KiB
HTML
356 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<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 (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"/>
|
||
<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;
|
||
height: calc(100vh - var(--BtnActionHeight));
|
||
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;
|
||
}
|
||
.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;
|
||
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);
|
||
}
|
||
</style>
|
||
<script>
|
||
var FrameZoomLevels = [50, 200];
|
||
var AppInfoString = `Frames Browser v2024-03-15.`;
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<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 onclick="ZoomFrame()">🔍️ Zoom</button></td>
|
||
<td><button onclick="ListFrames()">🪟 Frames</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>
|
||
<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>
|
||
<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>
|
||
<!--<p>You can also visit, or go back to, my home page: <a href="https://hub.octt.eu.org">hub.octt.eu.org</a>!</p>-->
|
||
<h2>Changelog</h2>
|
||
<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>
|
||
</div>
|
||
<script>
|
||
var CurrentFrames = [];
|
||
var FrameIndexes = [-1];
|
||
var FrameZoomIndex = -1;
|
||
var SampleHtmlContent = MainAppContent.innerHTML;
|
||
MainAppContent.innerHTML = '<iframe></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 ShowAppInfo(){
|
||
alert(AppInfoString);
|
||
};
|
||
|
||
function InputHandleKey(ev){
|
||
// Enter
|
||
if (ev.keyCode == 13) {
|
||
LoadFrame();
|
||
};
|
||
};
|
||
|
||
function ShowFrame(index){
|
||
var isFrameRoot = (index === -1);
|
||
ListFramesClose();
|
||
FrameIndexes = [index];
|
||
SaveCurrentFrames();
|
||
InputUri.disabled = isFrameRoot;
|
||
InputUri.value = (isFrameRoot ? '' : CurrentFrames[index]);
|
||
BtnFile.disabled = isFrameRoot;
|
||
BtnLoad.disabled = isFrameRoot;
|
||
BtnExcise.disabled = isFrameRoot;
|
||
document.querySelector('iframe').src = (isFrameRoot ? `data:text/html;utf8,${SampleHtmlContent}` : CurrentFrames[index]);
|
||
};
|
||
|
||
function SaveUrl(){
|
||
var url = document.querySelector('input[type="text"]').value;
|
||
localStorage.setItem('FramesBrowser.url', url);
|
||
CurrentFrames[FrameIndexes[0]] = url;
|
||
SaveCurrentFrames();
|
||
return url;
|
||
};
|
||
|
||
function AddFrame(){
|
||
CurrentFrames = CurrentFrames.concat(['']);
|
||
ListFrames();
|
||
ShowFrame(CurrentFrames.length - 1);
|
||
SaveCurrentFrames();
|
||
RefreshFramesCounter();
|
||
};
|
||
|
||
function CloseFrame(index){
|
||
CurrentFrames.pop(index);
|
||
if (FrameIndexes[0] === index) {
|
||
ShowFrame(-1);//ShowRootFrame();
|
||
} else if (FrameIndexes[0] > index) {
|
||
FrameIndexes[0] --;
|
||
};
|
||
ListFrames(); ListFrames();
|
||
SaveCurrentFrames();
|
||
RefreshFramesCounter();
|
||
};
|
||
|
||
function SaveCurrentFrames(){
|
||
localStorage.setItem('FramesBrowser.CurrentFrames', JSON.stringify(CurrentFrames));
|
||
localStorage.setItem('FramesBrowser.FrameIndexes', JSON.stringify(FrameIndexes));
|
||
};
|
||
|
||
function RefreshFramesCounter(){
|
||
var count = CurrentFrames.length;
|
||
var countHtml = '';
|
||
if (count > 99) {
|
||
countHtml = '(...)';
|
||
} else if (count > 0) {
|
||
countHtml = `(${count})`;
|
||
};
|
||
document.querySelector('button[onclick="ListFrames()"]').textContent = `🪟${countHtml} Frames`;
|
||
};
|
||
|
||
function LoadFrame(){
|
||
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 ZoomFrame(){
|
||
if (FrameZoomIndex === FrameZoomLevels.length - 1) {
|
||
FrameZoomIndex = -1;
|
||
} else {
|
||
FrameZoomIndex ++;
|
||
};
|
||
var level = FrameZoomLevels[FrameZoomIndex];
|
||
var levelopp = FrameZoomLevels[FrameZoomLevels.length - 1 - FrameZoomIndex];
|
||
var stylepos = (level < 100
|
||
? `right: ${level}vw; bottom: calc(${level}vh - 16px);`
|
||
: `left: ${levelopp/2}vw; top: calc(${levelopp/2}vh - 8px);`
|
||
);
|
||
document.querySelector('iframe').style = (FrameZoomIndex === -1
|
||
? ''
|
||
: `scale: ${level/100}; width: ${levelopp}vw; height: calc(${levelopp}vh - (var(--BtnActionHeight) * ${levelopp / 100})); ${stylepos}`
|
||
);
|
||
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 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');
|
||
Box.Content.appendChild(BoxList);
|
||
var LiMain = $new('li');
|
||
BoxList.appendChild(LiMain);
|
||
var BtnMain = $new('button', { innerHTML: 'Root Window', onclick: /*ShowRootFrame*/function(){ ShowFrame(-1) }, disabled: FrameIndexes[0] === -1 });
|
||
LiMain.appendChild(BtnMain);
|
||
for (var i=0; i<CurrentFrames.length; i++) {
|
||
var li = $new('li');
|
||
li.ItemIndex = i;
|
||
BoxList.appendChild(li);
|
||
var open = $new('button', { innerHTML: ` ${CurrentFrames[i].slice(0, 16)} `, onclick: function(){ShowFrame(this.parentElement.ItemIndex)}, disabled: FrameIndexes[0] === i });
|
||
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;
|
||
};
|
||
|
||
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 };
|
||
};
|
||
|
||
window.opendatauri = 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);
|
||
};
|
||
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');
|
||
};
|
||
|
||
window.onload = function(){
|
||
Array.from(document.querySelectorAll('noscript, .NoScript')).forEach(function(el){ el.remove() });
|
||
CurrentFrames = (JSON.parse(localStorage.getItem('FramesBrowser.CurrentFrames')) || []);
|
||
FrameIndexes = (JSON.parse(localStorage.getItem('FramesBrowser.FrameIndexes')) || [-1]);
|
||
document.querySelector('input[type="text"]').value = localStorage.getItem('FramesBrowser.url');
|
||
ShowFrame(FrameIndexes[0]);
|
||
RefreshFramesCounter();
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|