forked from tools/josh
1
0
Fork 0

Add navigation bar to top

Fixes #166.
This commit is contained in:
Florine W. Dekker 2021-04-22 16:19:15 +02:00
parent 57bcfcbf0f
commit 13d2164e89
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
7 changed files with 38 additions and 166 deletions

View File

@ -1,6 +1,6 @@
{
"name": "fwdekker.com",
"version": "0.39.20",
"version": "0.40.0",
"description": "The source code of [my personal website](https://fwdekker.com/).",
"author": "Felix W. Dekker",
"browser": "dist/bundle.js",

View File

@ -1,21 +1,21 @@
a {
#terminal a {
text-decoration: none;
}
a:link, a:visited {
#terminal a:link, #terminal a:visited {
color: #00FF00;
}
a:hover {
#terminal a:hover {
color: #00BF00;
text-decoration: underline;
}
a:link.dirLink, a:visited.dirLink {
#terminal a:link.dirLink, #terminal a:visited.dirLink {
color: #00FF00;
}
a:link.fileLink, a:visited.fileLink {
#terminal a:link.fileLink, #terminal a:visited.fileLink {
color: #FFFF00;
}
@ -51,18 +51,11 @@ a:link.fileLink, a:visited.fileLink {
body {
background-color: black;
overscroll-behavior-y: contain;
}
#terminal {
position: absolute;
right: 0;
bottom: 0;
left: 0;
min-height: 100%;
padding: 25px;
overflow: hidden;
cursor: text;
color: white;
@ -107,3 +100,9 @@ body {
.errorMessage {
color: #FF3333;
}
#nav {
position: sticky;
top: 0;
}

View File

@ -20,6 +20,7 @@
<link rel="apple-touch-icon" href="icon_ios.png?v=%%VERSION_NUMBER%%" />
<link rel="manifest" href="manifest.json?v=%%VERSION_NUMBER%%">
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css" />
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto-mono/roboto-mono.css" />
<!--suppress HtmlUnknownTarget -->
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
@ -28,6 +29,8 @@
</head>
<body>
<main>
<div id="nav"></div>
<!-- Comment out newlines and indents because of `white-space: pre-wrap` in CSS. -->
<div id="terminal"><!--
--><noscript><!--
@ -54,17 +57,18 @@
<script>
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
window.onload = function () {
window.onload = function() {
document.getElementById("ie-warning").style.display = "block";
};
}
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
window.addEventListener("load", function() {
return navigator.serviceWorker.register("sw.js?v=%%VERSION_NUMBER%%");
});
}
</script>
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js"></script>
<!--suppress HtmlUnknownTarget -->
<script type="module" src="bundle.js?v=%%VERSION_NUMBER%%"></script>
</body>

View File

@ -1,7 +1,9 @@
import * as semver from "semver";
// @ts-ignore
const {$, doAfterLoad, nav} = window.fwdekker;
import {FileSystem} from "./FileSystem";
import {Persistence} from "./Persistence";
import {addOnLoad, ExpectedGoodbyeError, q} from "./Shared";
import {ExpectedGoodbyeError} from "./Shared";
import {Terminal} from "./Terminal";
@ -24,7 +26,7 @@ declare global {
/**
* Compares version numbers to ensure no compatibility errors ensue.
*/
addOnLoad(() => {
doAfterLoad(() => {
const userVersion = Persistence.getVersion();
const latestVersion = "%%VERSION_NUMBER%%";
@ -36,7 +38,7 @@ addOnLoad(() => {
}
if (Persistence.getWasUpdated()) {
q("#terminalOutput").innerHTML = "" +
$("#terminalOutput").innerHTML = "" +
"<span style=\"color:red\">The terminal application has been updated. To prevent unexpected errors, all " +
"previous user changes have been reset.</span>\n\n";
Persistence.setWasUpdated(false);
@ -48,10 +50,10 @@ addOnLoad(() => {
/**
* Exits the application if the server is "shut down".
*/
addOnLoad(() => {
doAfterLoad(() => {
if (!Persistence.getPoweroff()) return;
q("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds.";
$("#terminalOutput").innerText = "Could not connect to fwdekker.com. Retrying in 10 seconds.";
setTimeout(() => location.reload(), 10000);
throw new ExpectedGoodbyeError("Goodbye");
});
@ -59,16 +61,17 @@ addOnLoad(() => {
/**
* Initializes the application.
*/
addOnLoad(async () => {
doAfterLoad(async () => {
$("#nav").appendChild(nav("/"));
if (!Persistence.hasFileSystem())
await FileSystem.loadNavApi();
window.terminal = new Terminal(
q("#terminal"),
q("#terminalInputField"),
q("#terminalOutput"),
q("#terminalInputPrefix"),
q("#terminalSuggestions")
$("#terminal"),
$("#terminalInputField"),
$("#terminalOutput"),
$("#terminalInputPrefix"),
$("#terminalSuggestions")
);
window.execute = (command: string) => window.terminal.processInput(command);

View File

@ -24,22 +24,6 @@ export const asciiHeaderHtml =
export const emptyFunction = () => {};
/**
* Runs the given function as soon as the page is done loading by "appending" it to the current definition of
* `window.onload`.
*
* @param fun the function to run as soon as the page is done loading
*/
export function addOnLoad(fun: () => void): void {
const oldOnLoad = window.onload ?? emptyFunction;
window.onload = () => {
// @ts-ignore: Call works without parameters as well
oldOnLoad();
fun();
};
}
/**
* Replaces all special HTML characters with escaped variants.
*
@ -122,41 +106,6 @@ export function moveCaretToEndOf(node: Node | null): void {
moveCaretTo(node, (node?.textContent ?? "").length);
}
/**
* Returns the number of pixels in a CSS value that describes a number of pixels, or `0` if the given string is `null`
* or does not contain a number.
*
* For example, if the given string is `"3px"`, this function will return `3`.
*
* @param string the CSS value to extract the number of pixels from
* @throws if the given string does not end with the text `"px"`
*/
export function parseCssPixels(string: string | null): number {
if (string === null || string.trim() === "") {
return 0;
} else {
if (!string.endsWith("px"))
throw new IllegalArgumentError("CSS string is not expressed in pixels.");
const result = parseFloat(string);
return isNaN(result) ? 0 : result;
}
}
/**
* Type-safe shorthand for `document.querySelector(query)`.
*
* @param query the query to run
* @throws if the element could not be found
*/
export function q(query: string): HTMLElement {
const element = document.querySelector(query);
if (!(element instanceof HTMLElement))
throw `Could not find element \`${query}\`.`;
return element;
}
/**
* Returns the longest common prefix of the given strings, or `undefined` if an empty array is given.
*

View File

@ -6,8 +6,7 @@ import {
findLongestCommonPrefix,
isStandalone,
moveCaretTo,
moveCaretToEndOf,
parseCssPixels
moveCaretToEndOf
} from "./Shared";
import {Shell} from "./Shell";
import {Buffer, StreamSet} from "./Stream";
@ -17,11 +16,6 @@ import {Buffer, StreamSet} from "./Stream";
* A terminal session that has input and output.
*/
export class Terminal {
/**
* The height of a single line in the output.
*/
private readonly lineHeight: number = 21; // TODO Calculate this dynamically
/**
* The HTML element of the terminal.
*/
@ -108,21 +102,6 @@ export class Terminal {
document.addEventListener("keydown", this.onkeydown.bind(this));
this.input.addEventListener("input", () => this.suggestionsText = "");
let scrollStartPosition: number = 0;
this.terminal.addEventListener("wheel", (event: WheelEvent) => {
this.scroll -= Math.sign(event.deltaY);
}, {passive: true});
this.terminal.addEventListener("touchstart", (event: TouchEvent) => {
scrollStartPosition = event.changedTouches[0].clientY;
}, {passive: true});
this.terminal.addEventListener("touchmove", (event: TouchEvent) => {
const newPosition = event.changedTouches[0].clientY;
const diff = scrollStartPosition - newPosition;
this.scroll -= diff / this.lineHeight;
scrollStartPosition = newPosition;
}, {passive: true});
this.outputText += this.shell.generateHeader();
this.prefixText += this.shell.generatePrefix();
this.input.focus();
@ -187,34 +166,6 @@ export class Terminal {
this.suggestions.innerHTML = suggestionsText;
}
/**
* Returns how many lines the user has scrolled up in the terminal.
*/
private get scroll(): number {
return -parseCssPixels(this.terminal.style.marginBottom) / this.lineHeight;
}
/**
* Sets the absolute number of lines to scroll up in the terminal relative to the bottom of the terminal.
*
* @param lines the absolute number of lines to scroll up in the terminal relative to the bottom of the terminal
*/
private set scroll(lines: number) {
const screenHeight = document.documentElement.clientHeight
- 2 * parseCssPixels(getComputedStyle(this.terminal).paddingTop); // top and bottom padding
const linesFitOnScreen = Math.round(screenHeight / this.lineHeight);
const linesInHistory = Math.round(this.output.offsetHeight / this.lineHeight) + 1; // +1 for input line
if (lines < 0)
lines = 0;
else if (linesInHistory <= linesFitOnScreen)
lines = 0;
else if (lines > linesInHistory - linesFitOnScreen)
lines = linesInHistory - linesFitOnScreen;
this.terminal.style.marginBottom = (-lines * this.lineHeight) + "px";
}
/**
* Returns `true` if and only if the input field does not display the user's input.
*/
@ -288,7 +239,7 @@ export class Terminal {
this.outputText += buffer;
this.prefixText = this.shell.generatePrefix();
this.scroll = 0;
this.input.scrollIntoView({behavior: "smooth"});
}
@ -351,7 +302,7 @@ export class Terminal {
case "os":
case "shift":
// Do nothing
return; // Return without scrolling to 0
return;
case "arrowup": {
// Display previous entry from history
this.inputText = this.inputHistory.previous();
@ -387,7 +338,7 @@ export class Terminal {
// Only if focused on the input as to not prevent copying of selected text
if (event.ctrlKey) {
if (this.input !== document.activeElement)
return; // Return without scrolling to 0
return;
this.ignoreInput();
event.preventDefault();
@ -421,7 +372,7 @@ export class Terminal {
break;
}
this.scroll = 0;
this.input.scrollIntoView({behavior: "smooth"});
}

View File

@ -1,7 +1,7 @@
import {expect} from "chai";
import "mocha";
import {escapeHtml, extractWordBefore, getFileExtension, parseCssPixels} from "../main/js/Shared";
import {escapeHtml, extractWordBefore, getFileExtension} from "../main/js/Shared";
describe("shared functions", () => {
@ -73,40 +73,6 @@ describe("shared functions", () => {
expect(getFileExtension("fi.le.ext")).to.equal("ext");
});
});
describe("parseCssPixels", () => {
it("returns 0 if null is given", () => {
expect(parseCssPixels(null)).to.equal(0);
});
it("returns 0 if an empty string is given", () => {
expect(parseCssPixels("")).to.equal(0);
});
it("returns 0 if a string containing only whitespace is given", () => {
expect(parseCssPixels(" ")).to.equal(0);
});
it("throws an error if the string does not end with 'px'", () => {
expect(() => parseCssPixels("12py")).to.throw();
});
it("returns 0 if the string does not contain a number", () => {
expect(parseCssPixels("errorpx")).to.equal(0);
});
it("returns the number contained in the string", () => {
expect(parseCssPixels("29px")).to.equal(29);
});
it("returns the number contained in the string even if surrounded with whitespace", () => {
expect(parseCssPixels(" 17 px")).to.equal(17);
});
it("returns a decimal number", () => {
expect(parseCssPixels("12.34px")).to.equal(12.34);
});
});
});
describe("extension functions", () => {