aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-07-25 05:14:06 +0700
committerShulhan <ms@kilabit.info>2021-07-26 02:06:40 +0700
commit1a49542d36fcc715b3839facee0ccac9952d5ecd (patch)
tree11b1b0d3ec1755774a46cd37698aa30a29c19b3c
downloadpakakeh.ts-1a49542d36fcc715b3839facee0ccac9952d5ecd.tar.xz
vfs: implement virtual file system explorer
The vfs.js implement the web user interface for virtual file system explorer.
-rw-r--r--.gitignore1
-rw-r--r--Makefile10
-rw-r--r--tsconfig.json12
-rw-r--r--vfs/example.html139
-rw-r--r--vfs/vfs.d.ts54
-rw-r--r--vfs/vfs.js173
-rw-r--r--vfs/vfs.ts238
7 files changed, 627 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07e6e47
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/node_modules
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..83b9780
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+OUT_JS= \
+ vfs/vfs.js
+
+.PHONY: all
+all: $(OUT_JS)
+ tsc --outDir .
+
+.PHONY: watch
+watch:
+ tsc --outDir . --watch
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..05d094d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "forceConsistentCasingInFileNames": true,
+ "inlineSourceMap": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES3"
+ },
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Recommended"
+}
diff --git a/vfs/example.html b/vfs/example.html
new file mode 100644
index 0000000..0fb1f82
--- /dev/null
+++ b/vfs/example.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>WUI - Virtual File System (vfs)</title>
+ </head>
+ <body onload="main()">
+ <div id="vfs"></div>
+
+ <script>
+ var exports = {}
+ </script>
+ <script src="vfs.js"></script>
+ <script>
+ let dummyfs = {
+ "/": {
+ name: "/",
+ path: "/",
+ is_dir: true,
+ childs: [
+ {
+ name: "Dir 1",
+ path: "/Dir 1",
+ is_dir: true,
+ childs: [
+ {
+ name: "File 1.1",
+ path: "/Dir 1/File 1.1",
+ },
+ {
+ name: "File 1.2",
+ path: "/Dir 1/File 1.2",
+ },
+ ],
+ },
+ {
+ name: "Dir 2",
+ path: "/Dir 2",
+ is_dir: true,
+ childs: [
+ {
+ name: "File 2.1",
+ path: "/Dir 2/File 2.1",
+ },
+ {
+ name: "File 2.2",
+ path: "/Dir 2/File 2.2",
+ },
+ ],
+ },
+ ],
+ },
+ "/Dir 1": {
+ name: "Dir 1",
+ path: "/Dir 1",
+ is_dir: true,
+ childs: [
+ {
+ name: "File 1.1",
+ path: "/Dir 1/File 1.1",
+ },
+ {
+ name: "File 1.2",
+ path: "/Dir 1/File 1.2",
+ },
+ ],
+ },
+ "/Dir 2": {
+ name: "Dir 2",
+ path: "/Dir 2",
+ is_dir: true,
+ childs: [
+ {
+ name: "File 2.1",
+ path: "/Dir 2/File 2.1",
+ },
+ {
+ name: "File 2.2",
+ path: "/Dir 2/File 2.2",
+ },
+ ],
+ },
+ }
+
+ function main() {
+ let opts = {
+ id: "vfs",
+ is_editable: true,
+ ListNodes: doListNodes,
+ GetNode: doGetNode,
+ }
+
+ let vfs = new Vfs(opts)
+ }
+
+ function doListNodes() {
+ let res = {
+ code: 200,
+ data: dummyfs,
+ }
+ return res
+ }
+
+ function doGetNode(path) {
+ let res = {
+ code: 200,
+ data: {
+ name: "",
+ path: path,
+ content: "",
+ },
+ }
+
+ switch (path) {
+ case "/Dir 1/File 1.1":
+ res.data.name = "File 1.1"
+ res.data.content = "This is the content of " + res.data.name
+ break
+ case "/Dir 1/File 1.2":
+ res.data.name = "File 1.2"
+ res.data.content = "This is the content of " + res.data.name
+ break
+ case "/Dir 2/File 2.1":
+ res.data.name = "File 2.1"
+ res.data.content = "This is the content of " + res.data.name
+ break
+ case "/Dir 2/File 2.2":
+ res.data.name = "File 2.1"
+ res.data.content = "This is the content of " + res.data.name
+ break
+ default:
+ res.code = 404
+ res.message = "path not found"
+ }
+ return res
+ }
+ </script>
+ </body>
+</html>
diff --git a/vfs/vfs.d.ts b/vfs/vfs.d.ts
new file mode 100644
index 0000000..eadc43e
--- /dev/null
+++ b/vfs/vfs.d.ts
@@ -0,0 +1,54 @@
+export interface IVfsNode {
+ name: string;
+ path: string;
+ is_dir: boolean;
+ mod_time_epoch?: number;
+ mod_time_rfc3339?: string;
+ size?: number;
+ mode?: string;
+ childs?: IVfsNode[];
+ content?: string;
+}
+export interface Response {
+ code: number;
+ message?: string;
+ data?: IVfsNode | IPathNode;
+}
+export interface VfsOptions {
+ id: string;
+ is_editable: boolean;
+ ListNodes: () => Response;
+ GetNode: (path: string) => Response;
+ UpdateNode: (node: Node) => Response;
+ DeleteNode: (node: Node) => Response;
+}
+export declare class Vfs {
+ opts: VfsOptions;
+ private el;
+ private comPath;
+ private comList;
+ private pathNode;
+ constructor(opts: VfsOptions);
+ onClickNode(this: Vfs, node: VfsNode): void;
+ onClickPath(this: Vfs, path: string): void;
+ open(node: VfsNode): void;
+}
+declare class VfsNode implements IVfsNode {
+ path: string;
+ name: string;
+ mod_time_epoch: number;
+ mod_time_rfc3339: string;
+ size: number;
+ mode: string;
+ is_dir: boolean;
+ childs: VfsNode[];
+ el: HTMLElement;
+ constructor(opts: IVfsNode, onClick: NodeClickHandler);
+ onMouseOut(t: VfsNode): void;
+ onMouseOver(t: VfsNode): void;
+}
+declare type IPathNode = {
+ [key: string]: IVfsNode;
+};
+declare type NodeClickHandler = (node: VfsNode) => void;
+export {};
diff --git a/vfs/vfs.js b/vfs/vfs.js
new file mode 100644
index 0000000..1c2242c
--- /dev/null
+++ b/vfs/vfs.js
@@ -0,0 +1,173 @@
+"use strict";
+exports.__esModule = true;
+exports.Vfs = void 0;
+var Vfs = /** @class */ (function () {
+ function Vfs(opts) {
+ var _this = this;
+ this.opts = opts;
+ this.el = null;
+ this.pathNode = {};
+ this.el = document.getElementById(opts.id);
+ if (!this.el) {
+ console.log("Vfs: element id", opts.id, "not found");
+ return;
+ }
+ var res = this.opts.ListNodes();
+ if (res.code != 200) {
+ console.log("Vfs: ListNodes: ", res.message);
+ return;
+ }
+ var resPathNode = res.data;
+ for (var key in resPathNode) {
+ var value = resPathNode[key];
+ var node = new VfsNode(value, function (node) {
+ _this.onClickNode(node);
+ });
+ this.pathNode[key] = node;
+ }
+ this.el.innerHTML = "";
+ this.comPath = new VfsPath(function (path) {
+ _this.onClickPath(path);
+ });
+ this.el.appendChild(this.comPath.el);
+ this.comList = new VfsList();
+ this.el.appendChild(this.comList.el);
+ this.open(this.pathNode["/"]);
+ }
+ Vfs.prototype.onClickNode = function (node) {
+ if (!node.is_dir) {
+ var res = this.opts.GetNode(node.path);
+ console.log("GetNode: ", res);
+ return;
+ }
+ this.comPath.open(node);
+ this.comList.open(node);
+ };
+ Vfs.prototype.onClickPath = function (path) {
+ var node = this.pathNode[path];
+ if (!node) {
+ console.log("Vfs: onClickPath: invalid path: ", path);
+ return;
+ }
+ this.open(node);
+ };
+ Vfs.prototype.open = function (node) {
+ this.comPath.open(node);
+ this.comList.open(node);
+ };
+ return Vfs;
+}());
+exports.Vfs = Vfs;
+var VfsNode = /** @class */ (function () {
+ function VfsNode(opts, onClick) {
+ var _this = this;
+ this.path = opts.path || "";
+ this.name = opts.name || "";
+ this.mod_time_epoch = opts.mod_time_epoch || 0;
+ this.mod_time_rfc3339 = opts.mod_time_rfc3339 || "";
+ this.size = opts.size || 0;
+ this.mode = opts.mode || "";
+ this.is_dir = opts.is_dir || false;
+ this.childs = [];
+ if (opts.childs !== undefined) {
+ for (var _i = 0, _a = opts.childs; _i < _a.length; _i++) {
+ var c = _a[_i];
+ this.childs.push(new VfsNode(c, onClick));
+ }
+ }
+ this.el = document.createElement("div");
+ this.el.style.padding = "1em";
+ this.el.style.cursor = "pointer";
+ this.el.innerHTML = this.name;
+ this.el.onclick = function (event) {
+ onClick(_this);
+ };
+ this.el.onmouseout = function (event) {
+ _this.onMouseOut(_this);
+ };
+ this.el.onmouseover = function (event) {
+ _this.onMouseOver(_this);
+ };
+ }
+ VfsNode.prototype.onMouseOut = function (t) {
+ t.el.style.backgroundColor = "white";
+ };
+ VfsNode.prototype.onMouseOver = function (t) {
+ t.el.style.backgroundColor = "aliceblue";
+ };
+ return VfsNode;
+}());
+var VfsList = /** @class */ (function () {
+ function VfsList() {
+ this.el = document.createElement("div");
+ this.el.style.borderWidth = "1px";
+ this.el.style.borderStyle = "solid";
+ this.el.style.borderColor = "black";
+ }
+ VfsList.prototype.open = function (node) {
+ this.el.innerHTML = "";
+ if (node.childs === undefined) {
+ return;
+ }
+ for (var _i = 0, _a = node.childs; _i < _a.length; _i++) {
+ var c = _a[_i];
+ this.el.appendChild(c.el);
+ }
+ };
+ return VfsList;
+}());
+var VfsPath = /** @class */ (function () {
+ function VfsPath(onClick) {
+ this.el = document.createElement("div");
+ this.el.style.borderWidth = "1px";
+ this.el.style.borderStyle = "solid";
+ this.el.style.borderColor = "black";
+ this.crumbs = [];
+ this.onClick = onClick;
+ }
+ VfsPath.prototype.open = function (node) {
+ var _this = this;
+ this.el.innerHTML = "";
+ this.crumbs = [];
+ var paths = [];
+ if (node.path == "/") {
+ paths.push(node.path);
+ }
+ else {
+ paths = node.path.split("/");
+ }
+ var _loop_1 = function (p) {
+ if (p == "") {
+ p = "/";
+ }
+ var crumb = document.createElement("span");
+ crumb.style.display = "inline-block";
+ crumb.style.padding = "1em";
+ crumb.style.cursor = "pointer";
+ crumb.innerHTML = p;
+ crumb.onclick = function (event) {
+ _this.onClick(p);
+ };
+ crumb.onmouseout = function (event) {
+ _this.onMouseOut(crumb, event);
+ };
+ crumb.onmouseover = function (event) {
+ _this.onMouseOver(crumb, event);
+ };
+ this_1.el.appendChild(crumb);
+ };
+ var this_1 = this;
+ for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) {
+ var p = paths_1[_i];
+ _loop_1(p);
+ }
+ };
+ VfsPath.prototype.onMouseOut = function (crumb, event) {
+ crumb.style.backgroundColor = "white";
+ };
+ VfsPath.prototype.onMouseOver = function (crumb, event) {
+ crumb.style.backgroundColor = "aliceblue";
+ };
+ return VfsPath;
+}());
+//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmZzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidmZzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQTZCQTtJQU1DLGFBQW1CLElBQWdCO1FBQW5DLGlCQWlDQztRQWpDa0IsU0FBSSxHQUFKLElBQUksQ0FBWTtRQUwzQixPQUFFLEdBQXVCLElBQUksQ0FBQTtRQUc3QixhQUFRLEdBQWEsRUFBRSxDQUFBO1FBRzlCLElBQUksQ0FBQyxFQUFFLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUE7UUFDMUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUU7WUFDYixPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixFQUFFLElBQUksQ0FBQyxFQUFFLEVBQUUsV0FBVyxDQUFDLENBQUE7WUFDcEQsT0FBTTtTQUNOO1FBRUQsSUFBSSxHQUFHLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQTtRQUMvQixJQUFJLEdBQUcsQ0FBQyxJQUFJLElBQUksR0FBRyxFQUFFO1lBQ3BCLE9BQU8sQ0FBQyxHQUFHLENBQUMsa0JBQWtCLEVBQUUsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFBO1lBQzVDLE9BQU07U0FDTjtRQUVELElBQUksV0FBVyxHQUFHLEdBQUcsQ0FBQyxJQUFpQixDQUFBO1FBQ3ZDLEtBQUssSUFBTSxHQUFHLElBQUksV0FBVyxFQUFFO1lBQzlCLElBQU0sS0FBSyxHQUFHLFdBQVcsQ0FBQyxHQUFHLENBQWEsQ0FBQTtZQUMxQyxJQUFNLElBQUksR0FBRyxJQUFJLE9BQU8sQ0FBQyxLQUFLLEVBQUUsVUFBQyxJQUFhO2dCQUM3QyxLQUFJLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxDQUFBO1lBQ3ZCLENBQUMsQ0FBQyxDQUFBO1lBQ0YsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUE7U0FDekI7UUFFRCxJQUFJLENBQUMsRUFBRSxDQUFD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 \ No newline at end of file
diff --git a/vfs/vfs.ts b/vfs/vfs.ts
new file mode 100644
index 0000000..7219c3a
--- /dev/null
+++ b/vfs/vfs.ts
@@ -0,0 +1,238 @@
+export interface IVfsNode {
+ name: string
+ path: string
+ is_dir: boolean
+
+ mod_time_epoch?: number
+ mod_time_rfc3339?: string
+ size?: number
+ mode?: string
+ childs?: IVfsNode[]
+ content?: string
+}
+
+export interface Response {
+ code: number
+ message?: string
+ data?: IVfsNode | IPathNode
+}
+
+export interface VfsOptions {
+ id: string
+ is_editable: boolean
+
+ ListNodes: () => Response
+ GetNode: (path: string) => Response
+ UpdateNode: (node: Node) => Response
+ DeleteNode: (node: Node) => Response
+}
+
+export class Vfs {
+ private el: HTMLElement | null = null
+ private comPath!: VfsPath
+ private comList!: VfsList
+ private pathNode: PathNode = {}
+
+ constructor(public opts: VfsOptions) {
+ this.el = document.getElementById(opts.id)
+ if (!this.el) {
+ console.log("Vfs: element id", opts.id, "not found")
+ return
+ }
+
+ let res = this.opts.ListNodes()
+ if (res.code != 200) {
+ console.log("Vfs: ListNodes: ", res.message)
+ return
+ }
+
+ let resPathNode = res.data as IPathNode
+ for (const key in resPathNode) {
+ const value = resPathNode[key] as IVfsNode
+ const node = new VfsNode(value, (node: VfsNode) => {
+ this.onClickNode(node)
+ })
+ this.pathNode[key] = node
+ }
+
+ this.el.innerHTML = ""
+
+ this.comPath = new VfsPath((path: string) => {
+ this.onClickPath(path)
+ })
+ this.el.appendChild(this.comPath.el)
+
+ this.comList = new VfsList()
+ this.el.appendChild(this.comList.el)
+
+ this.open(this.pathNode["/"])
+ }
+
+ onClickNode(this: Vfs, node: VfsNode) {
+ if (!node.is_dir) {
+ const res = this.opts.GetNode(node.path)
+ console.log("GetNode: ", res)
+ return
+ }
+ this.comPath.open(node)
+ this.comList.open(node)
+ }
+
+ onClickPath(this: Vfs, path: string) {
+ const node = this.pathNode[path]
+ if (!node) {
+ console.log("Vfs: onClickPath: invalid path: ", path)
+ return
+ }
+ this.open(node)
+ }
+
+ open(node: VfsNode) {
+ this.comPath.open(node)
+ this.comList.open(node)
+ }
+}
+
+class VfsNode implements IVfsNode {
+ path: string
+ name: string
+ mod_time_epoch: number
+ mod_time_rfc3339: string
+ size: number
+ mode: string
+ is_dir: boolean
+ childs: VfsNode[]
+
+ el: HTMLElement
+
+ constructor(opts: IVfsNode, onClick: NodeClickHandler) {
+ this.path = opts.path || ""
+ this.name = opts.name || ""
+ this.mod_time_epoch = opts.mod_time_epoch || 0
+ this.mod_time_rfc3339 = opts.mod_time_rfc3339 || ""
+ this.size = opts.size || 0
+ this.mode = opts.mode || ""
+ this.is_dir = opts.is_dir || false
+
+ this.childs = []
+ if (opts.childs !== undefined) {
+ for (let c of opts.childs) {
+ this.childs.push(new VfsNode(c, onClick))
+ }
+ }
+
+ this.el = document.createElement("div")
+ this.el.style.padding = "1em"
+ this.el.style.cursor = "pointer"
+ this.el.innerHTML = this.name
+
+ this.el.onclick = (event) => {
+ onClick(this)
+ }
+ this.el.onmouseout = (event) => {
+ this.onMouseOut(this)
+ }
+ this.el.onmouseover = (event) => {
+ this.onMouseOver(this)
+ }
+ }
+
+ onMouseOut(t: VfsNode) {
+ t.el.style.backgroundColor = "white"
+ }
+ onMouseOver(t: VfsNode) {
+ t.el.style.backgroundColor = "aliceblue"
+ }
+}
+
+class VfsList {
+ el: HTMLElement
+
+ constructor() {
+ this.el = document.createElement("div")
+ this.el.style.borderWidth = "1px"
+ this.el.style.borderStyle = "solid"
+ this.el.style.borderColor = "black"
+ }
+
+ open(node: VfsNode) {
+ this.el.innerHTML = ""
+
+ if (node.childs === undefined) {
+ return
+ }
+
+ for (let c of node.childs) {
+ this.el.appendChild(c.el)
+ }
+ }
+}
+
+class VfsPath {
+ el: HTMLElement
+ private crumbs: string[]
+ private onClick: PathClickHandler
+
+ constructor(onClick: PathClickHandler) {
+ this.el = document.createElement("div")
+ this.el.style.borderWidth = "1px"
+ this.el.style.borderStyle = "solid"
+ this.el.style.borderColor = "black"
+ this.crumbs = []
+ this.onClick = onClick
+ }
+
+ open(node: VfsNode) {
+ this.el.innerHTML = ""
+ this.crumbs = []
+ let paths = []
+
+ if (node.path == "/") {
+ paths.push(node.path)
+ } else {
+ paths = node.path.split("/")
+ }
+
+ for (let p of paths) {
+ if (p == "") {
+ p = "/"
+ }
+
+ let crumb = document.createElement("span")
+ crumb.style.display = "inline-block"
+ crumb.style.padding = "1em"
+ crumb.style.cursor = "pointer"
+ crumb.innerHTML = p
+
+ crumb.onclick = (event) => {
+ this.onClick(p)
+ }
+ crumb.onmouseout = (event) => {
+ this.onMouseOut(crumb, event)
+ }
+ crumb.onmouseover = (event) => {
+ this.onMouseOver(crumb, event)
+ }
+
+ this.el.appendChild(crumb)
+ }
+ }
+
+ onMouseOut(crumb: HTMLElement, event: MouseEvent) {
+ crumb.style.backgroundColor = "white"
+ }
+ onMouseOver(crumb: HTMLElement, event: MouseEvent) {
+ crumb.style.backgroundColor = "aliceblue"
+ }
+}
+
+type IPathNode = {
+ [key: string]: IVfsNode
+}
+
+type PathNode = {
+ [key: string]: VfsNode
+}
+
+type NodeClickHandler = (node: VfsNode) => void
+type PathClickHandler = (path: string) => void