aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--editor/editor.js205
-rw-r--r--editor/example.js65
-rw-r--r--input/checkboxes.js137
-rw-r--r--input/example.js224
-rw-r--r--input/file.js104
-rw-r--r--input/number.js170
-rw-r--r--input/option.js3
-rw-r--r--input/select.js152
-rw-r--r--input/string.js130
-rw-r--r--notif/example.js42
-rw-r--r--notif/notif.js64
-rw-r--r--response.js3
-rw-r--r--vfs/example.js145
-rw-r--r--vfs/vfs.js202
-rw-r--r--websocket_client.js110
16 files changed, 1758 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index b90bde9..376dcbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+//
// SPDX-License-Identifier: GPL-3.0-or-later
-*.js
/node_modules
-README.html
+/README.html
diff --git a/editor/editor.js b/editor/editor.js
new file mode 100644
index 0000000..e80e147
--- /dev/null
+++ b/editor/editor.js
@@ -0,0 +1,205 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const WUI_EDITOR_CLASS = "wui_editor";
+const WUI_EDITOR_CLASS_LINE_NUMBER = "wui_editor_line_number";
+const WUI_EDITOR_CLASS_CONTENT = "wui_editor_content";
+export class WuiEditor {
+ constructor(opts) {
+ this.opts = opts;
+ this.content = "";
+ this.totalLine = 0;
+ this.elLineNumber = document.createElement("div");
+ this.elContent = document.createElement("div");
+ this.isKeyControl = false;
+ this.id = opts.id;
+ this.isEditable = opts.isEditable;
+ const el = document.getElementById(opts.id);
+ if (!el) {
+ console.error("WuiEditor: element ID not found:", opts.id);
+ return;
+ }
+ this.el = el;
+ this.initStyle();
+ this.initLineNumber();
+ this.initContent();
+ this.el.classList.add(WUI_EDITOR_CLASS);
+ }
+ // getContent return content of file.
+ getContent() {
+ let content = "";
+ let el;
+ let line;
+ this.elContent.childNodes.forEach((node) => {
+ switch (node.nodeType) {
+ case Node.ELEMENT_NODE:
+ el = node;
+ line = el.innerText;
+ break;
+ case Node.TEXT_NODE:
+ line = node.nodeValue || "";
+ break;
+ }
+ line = line.trimEnd();
+ content += line + "\n";
+ });
+ content = content.trim();
+ return content;
+ }
+ // open the node for editing.
+ // The content MUST be encoded in base64.
+ open(node) {
+ this.content = atob(node.content);
+ this.content = this.content.replace("\r\n", "\n");
+ this.render(this.content);
+ }
+ setEditable(yes) {
+ this.isEditable = yes;
+ if (yes) {
+ this.elContent.setAttribute("contenteditable", "true");
+ }
+ else {
+ this.elContent.setAttribute("contenteditable", "false");
+ }
+ }
+ addNewLine() {
+ this.totalLine++;
+ const elLine = document.createElement("div");
+ elLine.innerText = `${this.totalLine}`;
+ this.elLineNumber.appendChild(elLine);
+ }
+ initLineNumber() {
+ this.elLineNumber.classList.add(WUI_EDITOR_CLASS_LINE_NUMBER);
+ this.el.appendChild(this.elLineNumber);
+ }
+ initContent() {
+ if (this.opts.isEditable) {
+ this.elContent.setAttribute("contenteditable", "true");
+ this.elContent.setAttribute("spellcheck", "false");
+ this.elContent.addEventListener("paste", (ev) => {
+ var _a;
+ ev.preventDefault();
+ let text = ((_a = ev.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text/plain")) || "";
+ if (!text) {
+ console.error(`on paste: text is ${text}`);
+ return;
+ }
+ const selection = window.getSelection();
+ if (!selection || !selection.rangeCount) {
+ console.error(`on paste: failed to get selection`);
+ return;
+ }
+ text = text.trimEnd();
+ selection.deleteFromDocument();
+ selection.getRangeAt(0).insertNode(document.createTextNode(text));
+ selection.collapseToEnd();
+ this.renderLineNumber(this.getContent());
+ });
+ this.elContent.onkeydown = (ev) => {
+ this.onKeydownDocument(this, ev);
+ };
+ this.elContent.onkeyup = (ev) => {
+ this.onKeyupDocument(this, ev);
+ };
+ this.elContent.addEventListener("blur", () => {
+ this.isKeyControl = false;
+ });
+ }
+ this.elContent.classList.add(WUI_EDITOR_CLASS_CONTENT);
+ this.el.appendChild(this.elContent);
+ }
+ initStyle() {
+ const style = document.createElement("style");
+ style.type = "text/css";
+ style.innerText = `
+ [contenteditable] {
+ outline: 0px solid transparent;
+ }
+ .${WUI_EDITOR_CLASS} {
+ background-color: cornsilk;
+ border: 1px solid brown;
+ font-family: monospace;
+ line-height: 1.6em;
+ overflow-y: scroll;
+ width: 100%;
+ }
+ .${WUI_EDITOR_CLASS_LINE_NUMBER} {
+ background-color: bisque;
+ border-right: 1px dashed brown;
+ color: dimgrey;
+ display: inline-block;
+ font-family: monospace;
+ margin-right: 4px;
+ padding: 0px 8px;
+ position: sticky;
+ text-align: right;
+ width: 3em;
+ }
+ .${WUI_EDITOR_CLASS_CONTENT} {
+ caret-color: red;
+ display: inline-block;
+ padding: 0px 8px 0 0;
+ vertical-align: top;
+ white-space: pre;
+ width: calc(100% - 10em);
+ word-wrap: normal;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ onKeydownDocument(ed, ev) {
+ switch (ev.key) {
+ case "Control":
+ ed.isKeyControl = true;
+ break;
+ case "Enter":
+ if (ed.isKeyControl) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (ed.opts.onSave) {
+ const content = ed.getContent();
+ ed.opts.onSave(content);
+ ed.render(content);
+ }
+ return false;
+ }
+ this.addNewLine();
+ return false;
+ }
+ return true;
+ }
+ onKeyupDocument(ed, ev) {
+ switch (ev.key) {
+ case "Control":
+ ed.isKeyControl = false;
+ return true;
+ }
+ return true;
+ }
+ render(content) {
+ const lines = content.split("\n");
+ this.elContent.innerText = "";
+ this.elLineNumber.innerText = "";
+ lines.forEach((line, x) => {
+ const el = document.createElement("div");
+ el.innerText = `${x + 1}`;
+ this.elLineNumber.appendChild(el);
+ const div = document.createElement("div");
+ div.innerText = line;
+ if (line == "") {
+ div.appendChild(document.createElement("br"));
+ }
+ this.elContent.appendChild(div);
+ });
+ this.totalLine = lines.length;
+ }
+ renderLineNumber(content) {
+ const lines = content.split("\n");
+ this.elLineNumber.innerText = "";
+ lines.forEach((_, x) => {
+ const el = document.createElement("div");
+ el.innerText = `${x + 1}`;
+ this.elLineNumber.appendChild(el);
+ });
+ this.totalLine = lines.length;
+ }
+}
diff --git a/editor/example.js b/editor/example.js
new file mode 100644
index 0000000..cbda425
--- /dev/null
+++ b/editor/example.js
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: 2019 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+import { WuiEditor } from "./editor.js";
+const nodeFile = {
+ name: "Test",
+ path: "/test",
+ is_dir: false,
+ content_type: "text/plain",
+ mod_time: 0,
+ size: 0,
+ mode: "",
+ childs: [],
+ content: btoa(`mkdir -p \${HOME}/aur/stackdriver-collectd
+
+git -C \${HOME}/aur/stackdriver-collectd clone \\
+ ssh://aur@aur.archlinux.org/stackdriver-collectd.git .
+
+sh -c "cd \${HOME}/aur/stackdriver-collectd; \\
+ makepkg --force --install --noconfirm"
+pacman -Ql stackdriver-collectd
+
+sudo systemctl enable stackdriver-collectd
+
+#put! {{.BaseDir}}/_template/etc/collectd-influxdb.conf /opt/collectd/etc/collectd.conf
+
+sudo systemctl restart stackdriver-collectd
+sudo systemctl status stackdriver-collectd
+
+##---- Connect telegraf with collectd
+
+{{.Val "influxdb::dir"}}/bin/influx bucket create \\
+ --name stackdriver_collectd \\
+ --description "stackdriver collectd" \\
+ --org {{.Val "influxdb::org"}} \\
+ --token {{.Val "influxdb:telegraf:token"}} \\
+ --retention "3d"
+
+#put: {{.BaseDir}}/_template/etc/telegraf/telegraf.d/stackdriver-collectd.conf \\
+ {{.Val "influxdb::dir"}}/etc/telegraf/telegraf.d/stackdriver-collectd.conf
+
+sudo systemctl restart telegraf
+sudo systemctl status telegraf
+`),
+};
+const opts = {
+ id: "editor",
+ isEditable: true,
+ onSave: (content) => {
+ const lines = content.split("\n");
+ lines.forEach((line, x) => {
+ console.log(`${x}: ${line}`);
+ });
+ },
+};
+const wuiEditor = new WuiEditor(opts);
+wuiEditor.open(nodeFile);
+const optsro = {
+ id: "editor-readonly",
+ isEditable: false,
+ onSave: (content) => {
+ console.log("OnSave: ", content);
+ },
+};
+const edro = new WuiEditor(optsro);
+edro.open(nodeFile);
diff --git a/input/checkboxes.js b/input/checkboxes.js
new file mode 100644
index 0000000..097f19c
--- /dev/null
+++ b/input/checkboxes.js
@@ -0,0 +1,137 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const WUI_INPUT_CHECKBOXES_CLASS = "wui_input_checkboxes";
+const WUI_INPUT_CHECKBOXES_CLASS_HINT = "wui_input_checkboxes_hint";
+const WUI_INPUT_CHECKBOXES_CLASS_HINT_TOGGLER = "wui_input_checkboxes_hint_toggler";
+const WUI_INPUT_CHECKBOXES_CLASS_INPUT = "wui_input_checkboxes_input";
+const WUI_INPUT_CHECKBOXES_CLASS_LABEL = "wui_input_checkboxes_label";
+//
+// WuiInputCheckboxes create an HTML input for selecting one or more item
+// using checkbox.
+//
+// Format of generated HTML output,
+//
+// <div [id=${id}] class="${WUI_INPUT_CHECKBOXES_CLASS}">
+// <label class="${WUI_INPUT_CHECKBOXES_CLASS_LABEL}">${label}</label>
+// [<span class="${WUI_INPUT_CHECKBOXES_CLASS_HINT_TOGGLER}">i </span>]
+// <fieldset
+// class="${WUI_INPUT_CHECKBOXES_CLASS_INPUT}"
+// [disabled=${is_disabled}]
+// >
+// ${ for key in options }
+// <div>
+// <input name=${name} value="${options[key].value}">
+// <label>${key}</label>
+// </div>
+// ${ endfor }
+// </fieldset>
+// [<div class="${WUI_INPUT_CHECKBOXES_CLASS_HINT}">${hint}</div>]
+// </div>
+//
+// The "hint" option is optional, if it set the input will have a hint toggler
+// to display or hide the input information.
+//
+// The onChangeHandler receive all checked values.
+//
+export class WuiInputCheckboxes {
+ constructor(opts) {
+ this.opts = opts;
+ this.values = [];
+ this.el = document.createElement("div");
+ if (opts.id) {
+ this.el.id = opts.id;
+ }
+ this.el.classList.add(WUI_INPUT_CHECKBOXES_CLASS);
+ this.el.style.padding = "2px";
+ this.generateLabel(this.el);
+ if (opts.hint) {
+ this.generateHintToggler(this.el);
+ }
+ this.generateInput(this.el);
+ if (opts.hint) {
+ this.generateHint();
+ }
+ }
+ generateLabel(wrapper) {
+ this.elLabel = document.createElement("label");
+ this.elLabel.classList.add(WUI_INPUT_CHECKBOXES_CLASS_LABEL);
+ this.elLabel.innerHTML = `${this.opts.label} `;
+ wrapper.appendChild(this.elLabel);
+ }
+ generateInput(wrapper) {
+ this.el_fieldset = document.createElement("fieldset");
+ this.el_fieldset.classList.add(WUI_INPUT_CHECKBOXES_CLASS_INPUT);
+ Object.entries(this.opts.options).forEach(([key, option]) => {
+ const value = option.value;
+ const wrapper = document.createElement("div");
+ const elCb = document.createElement("input");
+ elCb.type = "checkbox";
+ elCb.name = this.opts.name;
+ elCb.value = option.value;
+ if (option.selected) {
+ elCb.checked = true;
+ this.values.push(value);
+ }
+ elCb.onclick = () => {
+ this.onClickCheckbox(elCb.value, elCb.checked);
+ };
+ wrapper.appendChild(elCb);
+ const elLabel = document.createElement("label");
+ elLabel.innerHTML = key;
+ wrapper.appendChild(elLabel);
+ this.el_fieldset.appendChild(wrapper);
+ });
+ if (this.opts.is_disabled) {
+ this.el_fieldset.disabled = true;
+ }
+ wrapper.appendChild(this.el_fieldset);
+ }
+ generateHintToggler(wrapper) {
+ this.el_hint_toggler = document.createElement("span");
+ this.el_hint_toggler.classList.add(WUI_INPUT_CHECKBOXES_CLASS_HINT_TOGGLER);
+ this.el_hint_toggler.innerHTML = " &#x2139;";
+ this.el_hint_toggler.onmouseover = () => {
+ this.el_hint_toggler.style.cursor = "pointer";
+ };
+ this.el_hint_toggler.onclick = () => {
+ this.onClickHintToggler();
+ };
+ wrapper.appendChild(this.el_hint_toggler);
+ }
+ generateHint() {
+ this.el_hint = document.createElement("div");
+ this.el_hint.classList.add(WUI_INPUT_CHECKBOXES_CLASS_HINT);
+ this.el_hint.innerHTML = this.opts.hint || "";
+ if (this.opts.is_hint_toggled) {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ this.el_hint.style.borderRadius = "2px";
+ this.el_hint.style.padding = "4px";
+ this.el_hint.style.marginTop = "2px";
+ this.el.appendChild(this.el_hint);
+ }
+ onClickHintToggler() {
+ if (this.el_hint.style.display === "none") {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ }
+ onClickCheckbox(value, selected) {
+ for (let x = 0; x < this.values.length; x++) {
+ if (this.values[x] === value) {
+ this.values.splice(x, 1);
+ }
+ }
+ if (selected) {
+ this.values.push(value);
+ }
+ if (this.opts.onChangeHandler) {
+ this.opts.onChangeHandler(this.values);
+ }
+ }
+}
diff --git a/input/example.js b/input/example.js
new file mode 100644
index 0000000..92abba9
--- /dev/null
+++ b/input/example.js
@@ -0,0 +1,224 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+import { WuiInputFile } from "./file.js";
+import { WuiInputString } from "./string.js";
+import { WuiInputNumber } from "./number.js";
+import { WuiInputSelect } from "./select.js";
+import { WuiInputCheckboxes } from "./checkboxes.js";
+function exampleInputFile() {
+ const inputFile = new WuiInputFile();
+ inputFile.label = "Input file";
+ inputFile.hint = "Select file to be uploaded.";
+ inputFile.accept = "image/*";
+ inputFile.onChange = (file) => {
+ console.log(`Uploading ${file.name} with size ${file.size}, type ${file.type}, and last modified at ${file.lastModified}.`);
+ };
+ document.body.appendChild(inputFile.element());
+}
+function exampleInputString() {
+ const elExample = document.createElement("div");
+ const elTitle = document.createElement("h3");
+ elTitle.innerText = "Input string";
+ elExample.appendChild(elTitle);
+ const elOut = document.createElement("span");
+ let opts = {
+ id: "my_input_string",
+ label: "Input string with ID",
+ value: "Hello, input string",
+ hint: "The input ID is 'my_input_string'",
+ onChangeHandler: (v) => {
+ elOut.innerText = v;
+ },
+ };
+ let elInputString = new WuiInputString(opts);
+ elExample.appendChild(elInputString.el);
+ opts = {
+ label: "Input string disabled",
+ value: "Hello, disabled input string",
+ is_disabled: true,
+ hint: "The input string is disabled",
+ is_hint_toggled: true,
+ onChangeHandler: (v) => {
+ elOut.innerText = v;
+ },
+ };
+ elInputString = new WuiInputString(opts);
+ elExample.appendChild(elInputString.el);
+ opts = {
+ label: "Input string without hint",
+ value: "Hello, input string without hint",
+ onChangeHandler: (v) => {
+ elOut.innerText = v;
+ },
+ };
+ elInputString = new WuiInputString(opts);
+ elExample.appendChild(elInputString.el);
+ const elOutLabel = document.createElement("div");
+ elOutLabel.innerText = "Input string changes to ";
+ elOutLabel.appendChild(elOut);
+ elExample.appendChild(elOutLabel);
+ document.body.appendChild(elExample);
+}
+function exampleInputNumber() {
+ const elExample = document.createElement("div");
+ const elTitle = document.createElement("h3");
+ elTitle.innerText = "Input number";
+ elExample.appendChild(elTitle);
+ const elOut = document.createElement("span");
+ let opts = {
+ label: "Input number",
+ value: 1,
+ onChangeHandler: (val) => {
+ elOut.innerText = "" + val;
+ },
+ };
+ let inputNum = new WuiInputNumber(opts);
+ elExample.appendChild(inputNum.el);
+ opts = {
+ id: "my_input_number",
+ label: "Input number with ID",
+ value: 10,
+ hint: "The ID for this input is 'my_input_number'",
+ onChangeHandler: (val) => {
+ elOut.innerText = "" + val;
+ },
+ };
+ inputNum = new WuiInputNumber(opts);
+ elExample.appendChild(inputNum.el);
+ opts = {
+ label: "Input number disabled",
+ value: 1000,
+ hint: "Input number with 'is_disabled' set to true",
+ is_disabled: true,
+ is_hint_toggled: true,
+ onChangeHandler: (val) => {
+ elOut.innerText = "" + val;
+ },
+ };
+ inputNum = new WuiInputNumber(opts);
+ elExample.appendChild(inputNum.el);
+ opts = {
+ label: "Input number with hint",
+ value: 10000,
+ hint: "This is the <b>hint</b>",
+ onChangeHandler: (val) => {
+ elOut.innerText = "" + val;
+ },
+ };
+ inputNum = new WuiInputNumber(opts);
+ elExample.appendChild(inputNum.el);
+ opts = {
+ label: "Input number with max and min",
+ value: 10,
+ max: 12,
+ min: -20,
+ onChangeHandler: (val) => {
+ elOut.innerText = "" + val;
+ },
+ };
+ inputNum = new WuiInputNumber(opts);
+ elExample.appendChild(inputNum.el);
+ const elOutLabel = document.createElement("div");
+ elOutLabel.innerText = "Input number changes to ";
+ elOutLabel.appendChild(elOut);
+ elExample.appendChild(elOutLabel);
+ document.body.appendChild(elExample);
+}
+function exampleInputSelect() {
+ const elExample = document.createElement("div");
+ document.body.appendChild(elExample);
+ const elTitle = document.createElement("h3");
+ elTitle.innerText = "Input select";
+ elExample.appendChild(elTitle);
+ const elLog = document.createElement("div");
+ const opts = {
+ name: "my_fruit_price",
+ label: "Input select",
+ options: {
+ mango: {
+ value: "1000",
+ selected: false,
+ },
+ papaya: {
+ value: "200",
+ selected: false,
+ },
+ rambutan: {
+ value: "100",
+ selected: true,
+ },
+ },
+ hint: "Select one of the option to see the price.",
+ is_hint_toggled: true,
+ onChangeHandler: (key, value) => {
+ elLog.innerText = `The select input changes to '${key}' with price '${value}'`;
+ },
+ };
+ const input = new WuiInputSelect(opts);
+ elExample.appendChild(input.el);
+ elExample.appendChild(elLog);
+}
+function exampleInputCheckboxes() {
+ const elExample = document.createElement("div");
+ document.body.appendChild(elExample);
+ const elTitle = document.createElement("h3");
+ elTitle.innerText = "Input checkboxes";
+ elExample.appendChild(elTitle);
+ const elLog = document.createElement("div");
+ let opts = {
+ name: "my_fruits",
+ label: "Input checkboxes",
+ options: {
+ mango: {
+ value: "1000",
+ selected: false,
+ },
+ papaya: {
+ value: "200",
+ selected: false,
+ },
+ rambutan: {
+ value: "100",
+ selected: true,
+ },
+ },
+ hint: "Select fruits.",
+ onChangeHandler: (values) => {
+ elLog.innerText = `You are selecting ${values}`;
+ },
+ };
+ let input = new WuiInputCheckboxes(opts);
+ elExample.appendChild(input.el);
+ opts = {
+ name: "my_fruits",
+ label: "Input checkboxes",
+ options: {
+ mango: {
+ value: "1000",
+ selected: false,
+ },
+ papaya: {
+ value: "200",
+ selected: false,
+ },
+ rambutan: {
+ value: "100",
+ selected: true,
+ },
+ },
+ hint: "Input select with 'is_disabled' and 'is_hint_toggled' is set to 'true'.",
+ is_disabled: true,
+ is_hint_toggled: true,
+ onChangeHandler: (values) => {
+ elLog.innerText = `You are selecting ${values}`;
+ },
+ };
+ input = new WuiInputCheckboxes(opts);
+ elExample.appendChild(input.el);
+ elExample.appendChild(elLog);
+}
+exampleInputFile();
+exampleInputString();
+exampleInputNumber();
+exampleInputSelect();
+exampleInputCheckboxes();
diff --git a/input/file.js b/input/file.js
new file mode 100644
index 0000000..db33463
--- /dev/null
+++ b/input/file.js
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+export class WuiInputFile {
+ constructor() {
+ this.id = "";
+ this.class = "";
+ // Filter for files. For example "image/*" to allow selecting image files.
+ this.accept = "";
+ this.label = "";
+ this.hint = "";
+ this.classInput = ""; // Custom CSS class for input element.
+ this.classLabel = ""; // Custom CSS class for label.
+ this.classHint = ""; // Custom CSS class for hint text.
+ this.classHintToggle = ""; // Custom CSS class for hint toggler.
+ this.isDisabled = false;
+ this.isHintShowed = false;
+ this.elHint = document.createElement("div");
+ }
+ element() {
+ const el = document.createElement("div");
+ if (this.id) {
+ el.id = this.id;
+ }
+ el.classList.add("wui_input_file");
+ if (this.class) {
+ el.classList.add(this.class);
+ }
+ const wrapper = document.createElement("div");
+ this.generateLabel(wrapper);
+ this.generateInput(wrapper);
+ this.generateHintToggler(wrapper);
+ el.appendChild(wrapper);
+ this.generateHint(el);
+ return el;
+ }
+ generateLabel(wrapper) {
+ const elLabel = document.createElement("label");
+ elLabel.classList.add("wui_input_file_label");
+ if (this.classLabel) {
+ elLabel.classList.add(this.classLabel);
+ }
+ elLabel.innerHTML = `${this.label} `;
+ wrapper.appendChild(elLabel);
+ }
+ generateInput(wrapper) {
+ const elInput = document.createElement("input");
+ elInput.type = "file";
+ elInput.classList.add("wui_input_file_input");
+ if (this.classInput) {
+ elInput.classList.add(this.classInput);
+ }
+ if (this.accept) {
+ elInput.accept = this.accept;
+ }
+ if (this.isDisabled) {
+ elInput.disabled = true;
+ }
+ elInput.onchange = (event) => {
+ const file = event.target.files[0];
+ if (file && this.onChange) {
+ this.onChange(file);
+ }
+ };
+ wrapper.appendChild(elInput);
+ }
+ generateHintToggler(wrapper) {
+ const elHintToggler = document.createElement("span");
+ elHintToggler.classList.add("wui_input_file_hint_toggler");
+ if (this.classHintToggle) {
+ elHintToggler.classList.add(this.classHintToggle);
+ }
+ elHintToggler.innerHTML = " &#x2139;";
+ elHintToggler.onmouseover = () => {
+ elHintToggler.style.cursor = "pointer";
+ };
+ elHintToggler.onclick = () => {
+ if (this.elHint.style.display === "none") {
+ this.elHint.style.display = "block";
+ }
+ else {
+ this.elHint.style.display = "none";
+ }
+ };
+ wrapper.appendChild(elHintToggler);
+ }
+ generateHint(parent) {
+ this.elHint = document.createElement("div");
+ this.elHint.classList.add("wui_input_file_hint");
+ if (this.classHint) {
+ this.elHint.classList.add(this.classHint);
+ }
+ this.elHint.innerHTML = this.hint;
+ this.elHint.style.borderRadius = "2px";
+ this.elHint.style.padding = "4px";
+ this.elHint.style.marginTop = "2px";
+ if (this.isHintShowed) {
+ this.elHint.style.display = "block";
+ }
+ else {
+ this.elHint.style.display = "none";
+ }
+ parent.appendChild(this.elHint);
+ }
+}
diff --git a/input/number.js b/input/number.js
new file mode 100644
index 0000000..135066c
--- /dev/null
+++ b/input/number.js
@@ -0,0 +1,170 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const WUI_INPUT_NUMBER_CLASS = "wui_input_number";
+const WUI_INPUT_NUMBER_CLASS_HINT = "wui_input_number_hint";
+const WUI_INPUT_NUMBER_CLASS_HINT_TOGGLER = "wui_input_number_hint_toggler";
+const WUI_INPUT_NUMBER_CLASS_INPUT = "wui_input_number_input";
+const WUI_INPUT_NUMBER_CLASS_LABEL = "wui_input_number_label";
+//
+// WuiInputNumber create an HTML input that allow number only, with optional
+// max and min options.
+// The required options are "label" and "value".
+//
+// Format of generated HTML output,
+//
+// <div [id=${id}] class="${WUI_INPUT_NUMBER_CLASS}">
+// <div>
+// <label class="${WUI_INPUT_NUMBER_CLASS_LABEL} [${class_label}]">
+// ${label} | HTMLElement
+// </label>
+// <input
+// class="${WUI_INPUT_NUMBER_CLASS_INPUT} [${class_input}]"
+// [max=${max}]
+// [min=${min}]
+// [disabled=${is_disabled}]
+// value=${value}
+// >
+// [<span class="${WUI_INPUT_NUMBER_CLASS_HINT_TOGGLER}"> i</span>]
+// </div>
+// [<div class="${WUI_INPUT_NUMBER_CLASS_HINT}">${hint}</div>]
+// </div>
+//
+// User can set onChangeHandler to receive new value when the input value
+// changes and valid; otherwise, if the value is invalid, the input
+// background will changes accordingly.
+//
+export class WuiInputNumber {
+ constructor(opts) {
+ this.opts = opts;
+ this.value = 0;
+ this.value = opts.value;
+ this.el = document.createElement("div");
+ if (opts.id) {
+ this.el.id = opts.id;
+ }
+ this.el.classList.add(WUI_INPUT_NUMBER_CLASS);
+ this.el.style.padding = "2px";
+ const wrapper = document.createElement("div");
+ this.generateLabel(wrapper);
+ this.generateInput(wrapper);
+ if (opts.hint) {
+ this.generateHintToggler(wrapper);
+ }
+ this.el.appendChild(wrapper);
+ if (opts.hint) {
+ this.generateHint();
+ }
+ }
+ generateLabel(wrapper) {
+ this.el_label = document.createElement("label");
+ this.el_label.classList.add(WUI_INPUT_NUMBER_CLASS_LABEL);
+ if (this.opts.class_label) {
+ this.el_label.classList.add(this.opts.class_label);
+ }
+ if (typeof this.opts.label === "string") {
+ this.el_label.innerHTML = `${this.opts.label} `;
+ }
+ else {
+ this.el_label.appendChild(this.opts.label);
+ }
+ wrapper.appendChild(this.el_label);
+ }
+ generateInput(wrapper) {
+ this.el_input = document.createElement("input");
+ this.el_input.classList.add(WUI_INPUT_NUMBER_CLASS_INPUT);
+ if (this.opts.class_input) {
+ this.el_input.classList.add(this.opts.class_input);
+ }
+ this.el_input.type = "number";
+ this.el_input.step = "any";
+ this.el_input.value = "" + this.opts.value;
+ let hint = "";
+ if (this.opts.max) {
+ this.el_input.max = "" + this.opts.max;
+ hint = "The maximum value is " + this.opts.max;
+ }
+ if (this.opts.min) {
+ this.el_input.min = "" + this.opts.min;
+ if (hint == "") {
+ hint = "The ";
+ }
+ else {
+ hint += " and ";
+ }
+ hint += "minimum value is " + this.opts.min;
+ }
+ if (hint !== "") {
+ this.el_input.title = hint;
+ }
+ if (this.opts.is_disabled) {
+ this.el_input.disabled = true;
+ }
+ this.el_input.onkeyup = (ev) => {
+ return this.onKeyUp(ev);
+ };
+ wrapper.appendChild(this.el_input);
+ }
+ generateHintToggler(wrapper) {
+ this.el_hint_toggler = document.createElement("span");
+ this.el_hint_toggler.classList.add(WUI_INPUT_NUMBER_CLASS_HINT_TOGGLER);
+ this.el_hint_toggler.innerHTML = " &#x2139;";
+ this.el_hint_toggler.onmouseover = () => {
+ this.el_hint_toggler.style.cursor = "pointer";
+ };
+ this.el_hint_toggler.onclick = () => {
+ this.onClickHintToggler();
+ };
+ wrapper.appendChild(this.el_hint_toggler);
+ }
+ generateHint() {
+ const hint = this.opts.hint || "";
+ this.el_hint = document.createElement("div");
+ this.el_hint.classList.add(WUI_INPUT_NUMBER_CLASS_HINT);
+ this.el_hint.innerHTML = hint;
+ if (this.opts.is_hint_toggled) {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ this.el_hint.style.borderRadius = "2px";
+ this.el_hint.style.padding = "4px";
+ this.el_hint.style.marginTop = "2px";
+ this.el.appendChild(this.el_hint);
+ }
+ onClickHintToggler() {
+ if (this.el_hint.style.display === "none") {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ }
+ onKeyUp(ev) {
+ const target = ev.target;
+ ev.preventDefault();
+ const newValue = +target.value;
+ if (newValue === null) {
+ this.el_input.style.backgroundColor = "lightsalmon";
+ return false;
+ }
+ if (this.opts.max && newValue > this.opts.max) {
+ this.el_input.style.backgroundColor = "lightsalmon";
+ return false;
+ }
+ if (this.opts.min && newValue < this.opts.min) {
+ this.el_input.style.backgroundColor = "lightsalmon";
+ return false;
+ }
+ this.el_input.style.backgroundColor = "white";
+ this.value = newValue;
+ if (this.opts.onChangeHandler) {
+ this.opts.onChangeHandler(this.value);
+ }
+ return true;
+ }
+ // set the input value.
+ set(v) {
+ this.el_input.value = "" + v;
+ }
+}
diff --git a/input/option.js b/input/option.js
new file mode 100644
index 0000000..173bec3
--- /dev/null
+++ b/input/option.js
@@ -0,0 +1,3 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+export {};
diff --git a/input/select.js b/input/select.js
new file mode 100644
index 0000000..ba8b919
--- /dev/null
+++ b/input/select.js
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const WUI_INPUT_SELECT_CLASS = "wui_input_select";
+const WUI_INPUT_SELECT_CLASS_HINT = "wui_input_select_hint";
+const WUI_INPUT_SELECT_CLASS_HINT_TOGGLER = "wui_input_select_hint_toggler";
+const WUI_INPUT_SELECT_CLASS_INPUT = "wui_input_select_input";
+const WUI_INPUT_SELECT_CLASS_LABEL = "wui_input_select_label";
+// WuiInputSelect create an HTML input for selecting one more item.
+//
+// Format of generated HTML output,
+//
+// <div [id=${id}] class="${WUI_INPUT_SELECT_CLASS}">
+// <div>
+// <label class="${WUI_INPUT_SELECT_CLASS_LABEL} [${class_label}]">
+// ${label} | HTMLElement
+// </label>
+// <select
+// name=${name}
+// class="${WUI_INPUT_SELECT_CLASS_INPUT} [${class_input}]"
+// [disabled=${is_disabled}]
+// >
+// <option value="${options[key].value}">${key in options}</option>
+// </select>
+// [<span class="${WUI_INPUT_SELECT_CLASS_HINT_TOGGLER}">i </span>]
+// </div>
+// [<div class="${WUI_INPUT_SELECT_CLASS_HINT}">${hint}</div>]
+// </div>
+//
+// The "hint" option is optional, if it set the input will have a hint toggler
+// to display or hide the input information.
+//
+// User can set onChangeHandler to receive new value when the input value
+// changes.
+export class WuiInputSelect {
+ constructor(opts) {
+ this.opts = opts;
+ this.value_key = {}; // The reverse of options.options
+ this.value = "";
+ this.el = document.createElement("div");
+ if (opts.id) {
+ this.el.id = opts.id;
+ }
+ this.el.classList.add(WUI_INPUT_SELECT_CLASS);
+ this.el.style.padding = "2px";
+ const wrapper = document.createElement("div");
+ this.generateLabel(wrapper);
+ this.generateInput(wrapper);
+ if (opts.hint) {
+ this.generateHintToggler(wrapper);
+ }
+ this.el.appendChild(wrapper);
+ if (opts.hint) {
+ this.generateHint();
+ }
+ }
+ generateLabel(wrapper) {
+ this.el_label = document.createElement("label");
+ this.el_label.classList.add(WUI_INPUT_SELECT_CLASS_LABEL);
+ if (this.opts.class_label) {
+ this.el_label.classList.add(this.opts.class_label);
+ }
+ if (typeof this.opts.label === "string") {
+ this.el_label.innerHTML = `${this.opts.label} `;
+ }
+ else {
+ this.el_label.appendChild(this.opts.label);
+ }
+ wrapper.appendChild(this.el_label);
+ }
+ generateInput(wrapper) {
+ this.el_input = document.createElement("select");
+ this.el_input.name = this.opts.name;
+ this.el_input.classList.add(WUI_INPUT_SELECT_CLASS_INPUT);
+ if (this.opts.class_input) {
+ this.el_input.classList.add(this.opts.class_input);
+ }
+ Object.entries(this.opts.options).forEach(([key, option]) => {
+ const elOption = document.createElement("option");
+ elOption.value = option.value;
+ elOption.innerHTML = key;
+ if (option.selected) {
+ elOption.selected = true;
+ }
+ this.el_input.appendChild(elOption);
+ this.value_key[option.value] = key;
+ });
+ if (this.opts.is_disabled) {
+ this.el_input.disabled = true;
+ }
+ if (this.opts.onChangeHandler) {
+ this.el_input.onclick = (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.onClickInput();
+ };
+ }
+ wrapper.appendChild(this.el_input);
+ }
+ generateHintToggler(wrapper) {
+ this.el_hint_toggler = document.createElement("span");
+ this.el_hint_toggler.classList.add(WUI_INPUT_SELECT_CLASS_HINT_TOGGLER);
+ this.el_hint_toggler.innerHTML = " &#x2139;";
+ this.el_hint_toggler.onmouseover = () => {
+ this.el_hint_toggler.style.cursor = "pointer";
+ };
+ this.el_hint_toggler.onclick = () => {
+ this.onClickHintToggler();
+ };
+ wrapper.appendChild(this.el_hint_toggler);
+ }
+ generateHint() {
+ this.el_hint = document.createElement("div");
+ this.el_hint.classList.add(WUI_INPUT_SELECT_CLASS_HINT);
+ this.el_hint.innerHTML = this.opts.hint || "";
+ if (this.opts.is_hint_toggled) {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ this.el_hint.style.borderRadius = "2px";
+ this.el_hint.style.padding = "4px";
+ this.el_hint.style.marginTop = "2px";
+ this.el.appendChild(this.el_hint);
+ }
+ onClickHintToggler() {
+ if (this.el_hint.style.display === "none") {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ }
+ onClickInput() {
+ if (!this.opts.onChangeHandler) {
+ return false;
+ }
+ const value = this.el_input.value;
+ if (this.value !== value) {
+ const key = this.value_key[value];
+ if (key) {
+ this.opts.onChangeHandler(key, value);
+ this.value = value;
+ }
+ }
+ return true;
+ }
+ // set the input value.
+ set(v) {
+ this.el_input.value = v;
+ }
+}
diff --git a/input/string.js b/input/string.js
new file mode 100644
index 0000000..437d66b
--- /dev/null
+++ b/input/string.js
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const WUI_INPUT_STRING_CLASS = "wui_input_string";
+const WUI_INPUT_STRING_CLASS_HINT = "wui_input_string_hint";
+const WUI_INPUT_STRING_CLASS_HINT_TOGGLER = "wui_input_string_hint_toggler";
+const WUI_INPUT_STRING_CLASS_INPUT = "wui_input_string_input";
+const WUI_INPUT_STRING_CLASS_LABEL = "wui_input_string_label";
+// WuiInputString create an HTML input for string with predefined options.
+// The required options are "label" and "value".
+//
+// Format of generated HTML output,
+//
+// <div [id=${id}] class="${WUI_INPUT_STRING_CLASS}">
+// <div>
+// <label class="${WUI_INPUT_STRING_CLASS_LABEL} [${class_label}]">
+// ${label} | HTMLElement
+// </label>
+// <input
+// class="${WUI_INPUT_STRING_CLASS_INPUT} [${class_input}]"
+// [disabled=${is_disabled}]
+// value=${value}
+// >
+// [<span class="${WUI_INPUT_STRING_CLASS_HINT_TOGGLER}">i </span>]
+// </div>
+// [<div class="${WUI_INPUT_STRING_CLASS_HINT}">${hint}</div>]
+// </div>
+//
+// The "hint" option is optional, if it set the input will have a hint toggler
+// to display or hide the input information.
+//
+// User can set onChangeHandler to receive new value when the input value
+// changes.
+export class WuiInputString {
+ constructor(opts) {
+ this.opts = opts;
+ this.value = "";
+ this.value = opts.value;
+ this.el = document.createElement("div");
+ if (opts.id) {
+ this.el.id = opts.id;
+ }
+ this.el.classList.add(WUI_INPUT_STRING_CLASS);
+ this.el.style.padding = "2px";
+ const wrapper = document.createElement("div");
+ this.generateLabel(wrapper);
+ this.generateInput(wrapper);
+ if (opts.hint) {
+ this.generateHintToggler(wrapper);
+ }
+ this.el.appendChild(wrapper);
+ if (opts.hint) {
+ this.generateHint();
+ }
+ }
+ generateLabel(wrapper) {
+ this.el_label = document.createElement("label");
+ this.el_label.classList.add(WUI_INPUT_STRING_CLASS_LABEL);
+ if (this.opts.class_label) {
+ this.el_label.classList.add(this.opts.class_label);
+ }
+ if (typeof this.opts.label === "string") {
+ this.el_label.innerHTML = `${this.opts.label} `;
+ }
+ else {
+ this.el_label.appendChild(this.opts.label);
+ }
+ wrapper.appendChild(this.el_label);
+ }
+ generateInput(wrapper) {
+ this.el_input = document.createElement("input");
+ this.el_input.classList.add(WUI_INPUT_STRING_CLASS_INPUT);
+ if (this.opts.class_input) {
+ this.el_input.classList.add(this.opts.class_input);
+ }
+ this.el_input.value = "" + this.opts.value;
+ if (this.opts.is_disabled) {
+ this.el_input.disabled = true;
+ }
+ if (this.opts.onChangeHandler) {
+ this.el_input.onkeyup = () => {
+ if (this.opts.onChangeHandler) {
+ if (this.value !== this.el_input.value) {
+ this.opts.onChangeHandler(this.el_input.value);
+ this.value = this.el_input.value;
+ }
+ }
+ };
+ }
+ wrapper.appendChild(this.el_input);
+ }
+ generateHintToggler(wrapper) {
+ this.el_hint_toggler = document.createElement("span");
+ this.el_hint_toggler.classList.add(WUI_INPUT_STRING_CLASS_HINT_TOGGLER);
+ this.el_hint_toggler.innerHTML = " &#x2139;";
+ this.el_hint_toggler.onmouseover = () => {
+ this.el_hint_toggler.style.cursor = "pointer";
+ };
+ this.el_hint_toggler.onclick = () => {
+ this.onClickHintToggler();
+ };
+ wrapper.appendChild(this.el_hint_toggler);
+ }
+ generateHint() {
+ this.el_hint = document.createElement("div");
+ this.el_hint.classList.add(WUI_INPUT_STRING_CLASS_HINT);
+ this.el_hint.innerHTML = this.opts.hint || "";
+ if (this.opts.is_hint_toggled) {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ this.el_hint.style.borderRadius = "2px";
+ this.el_hint.style.padding = "4px";
+ this.el_hint.style.marginTop = "2px";
+ this.el.appendChild(this.el_hint);
+ }
+ onClickHintToggler() {
+ if (this.el_hint.style.display === "none") {
+ this.el_hint.style.display = "block";
+ }
+ else {
+ this.el_hint.style.display = "none";
+ }
+ }
+ // set the input value.
+ set(v) {
+ this.el_input.value = v;
+ }
+}
diff --git a/notif/example.js b/notif/example.js
new file mode 100644
index 0000000..1a543fd
--- /dev/null
+++ b/notif/example.js
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+import { WuiNotif, WUI_NOTIF_CLASS_ERROR, WUI_NOTIF_CLASS_INFO, } from "./notif.js";
+let inputMsg;
+let wuiNotif;
+function main() {
+ wuiNotif = new WuiNotif();
+ inputMsg = document.createElement("textarea");
+ inputMsg.id = "input_msg";
+ inputMsg.value =
+ "Test notification with HTML format using <b>bold</b> and <u>underline</u> words.";
+ document.body.appendChild(inputMsg);
+ const elWrapper = document.createElement("div");
+ elWrapper.style.marginTop = "10px";
+ document.body.appendChild(elWrapper);
+ const elButtonInfo = document.createElement("button");
+ elButtonInfo.innerText = "Info";
+ elButtonInfo.style.marginRight = "10px";
+ elButtonInfo.onclick = notifInfo;
+ elWrapper.appendChild(elButtonInfo);
+ const elButtonError = document.createElement("button");
+ elButtonError.innerText = "Error";
+ elButtonError.onclick = notifError;
+ elWrapper.appendChild(elButtonError);
+ document.body.appendChild(document.createElement("p"));
+ const previewError = document.createElement("div");
+ previewError.classList.add(`${WUI_NOTIF_CLASS_ERROR}`);
+ previewError.innerText = `Preview of error style`;
+ document.body.appendChild(previewError);
+ const previewInfo = document.createElement("div");
+ previewInfo.classList.add(`${WUI_NOTIF_CLASS_INFO}`);
+ previewInfo.innerText = `Preview of info style`;
+ document.body.appendChild(previewInfo);
+}
+function notifInfo() {
+ wuiNotif.info(inputMsg.value);
+}
+function notifError() {
+ wuiNotif.error(inputMsg.value);
+}
+// ----
+main();
diff --git a/notif/notif.js b/notif/notif.js
new file mode 100644
index 0000000..2070f9e
--- /dev/null
+++ b/notif/notif.js
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+export const WUI_NOTIF_ID = "wui_notif";
+export const WUI_NOTIF_CLASS_INFO = "wui_notif_info";
+export const WUI_NOTIF_CLASS_ERROR = "wui_notif_error";
+// WuiNotif implement the HTML interface to display pop-up notification.
+// The notification can be triggered by calling method info() or error().
+// Each pop-up has 5 seconds duration, after that they will be removed
+// automatically.
+export class WuiNotif {
+ constructor() {
+ this.timeout = 5000; // 5 seconds timeout
+ this.el = document.createElement("div");
+ this.el.id = WUI_NOTIF_ID;
+ document.body.appendChild(this.el);
+ this.initStyle();
+ }
+ // info show the msg as information.
+ info(msg) {
+ const item = document.createElement("div");
+ item.innerHTML = msg;
+ item.classList.add(WUI_NOTIF_CLASS_INFO);
+ this.el.appendChild(item);
+ setTimeout(() => {
+ this.el.removeChild(item);
+ }, this.timeout);
+ }
+ // error show the msg as an error.
+ error(msg) {
+ const item = document.createElement("div");
+ item.innerHTML = msg;
+ item.classList.add(WUI_NOTIF_CLASS_ERROR);
+ this.el.appendChild(item);
+ setTimeout(() => {
+ this.el.removeChild(item);
+ }, this.timeout);
+ }
+ initStyle() {
+ const style = document.createElement("style");
+ style.type = "text/css";
+ style.innerText = `
+ #${WUI_NOTIF_ID} {
+ left: 10%;
+ position: fixed;
+ top: 1em;
+ width: 80%;
+ z-index: 10000;
+ }
+ .${WUI_NOTIF_CLASS_INFO} {
+ border: 1px solid silver;
+ background-color: honeydew;
+ margin-bottom: 1em;
+ padding: 1em;
+ }
+ .${WUI_NOTIF_CLASS_ERROR} {
+ border: 1px solid salmon;
+ background-color: lightsalmon;
+ margin-bottom: 1em;
+ padding: 1em;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+}
diff --git a/response.js b/response.js
new file mode 100644
index 0000000..173bec3
--- /dev/null
+++ b/response.js
@@ -0,0 +1,3 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+export {};
diff --git a/vfs/example.js b/vfs/example.js
new file mode 100644
index 0000000..ea910f2
--- /dev/null
+++ b/vfs/example.js
@@ -0,0 +1,145 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+import { WuiVfs } from "./vfs.js";
+const dummyfs = {
+ "/": {
+ name: "/",
+ path: "/",
+ is_dir: true,
+ content: "",
+ childs: [
+ {
+ name: "Dir 1",
+ path: "/Dir 1",
+ is_dir: true,
+ content: "",
+ childs: [
+ {
+ name: "File 1.1",
+ path: "/Dir 1/File 1.1",
+ is_dir: false,
+ content: "This is the content of File 1.1",
+ },
+ {
+ name: `File 1.2`,
+ path: "/Dir 1/File 1.2",
+ is_dir: false,
+ content: "This is the content of File 1.2",
+ },
+ ],
+ },
+ {
+ name: "Dir 2",
+ path: "/Dir 2",
+ is_dir: true,
+ content: "",
+ childs: [
+ {
+ name: "File 2.1",
+ path: "/Dir 2/File 2.1",
+ is_dir: false,
+ content: "This is the content of File 2.1",
+ },
+ {
+ name: "File 2.2",
+ path: "/Dir 2/File 2.2",
+ is_dir: false,
+ content: "This is the content of File 2.2",
+ },
+ ],
+ },
+ ],
+ },
+ "/Dir 1": {
+ name: "Dir 1",
+ path: "/Dir 1",
+ is_dir: true,
+ content: "",
+ childs: [
+ {
+ name: "File 1.1",
+ path: "/Dir 1/File 1.1",
+ is_dir: false,
+ content: "This is the content of File 1.1",
+ },
+ {
+ name: "File 1.2",
+ path: "/Dir 1/File 1.2",
+ is_dir: false,
+ content: "This is the content of File 1.2",
+ },
+ ],
+ },
+ "/Dir 2": {
+ name: "Dir 2",
+ path: "/Dir 2",
+ is_dir: true,
+ content: "",
+ childs: [
+ {
+ name: "File 2.1",
+ path: "/Dir 2/File 2.1",
+ is_dir: false,
+ content: "This is the content of File 2.1",
+ },
+ {
+ name: "File 2.2",
+ path: "/Dir 2/File 2.2",
+ is_dir: false,
+ content: "This is the content of File 2.2",
+ },
+ ],
+ },
+};
+async function main() {
+ const opts = {
+ id: "vfs",
+ open: open,
+ openNode: openNode,
+ };
+ const wuiVFS = new WuiVfs(opts);
+ wuiVFS.openDir("/");
+}
+async function open(path, isDir) {
+ console.log("Open:", path, isDir);
+ const res = {
+ code: 200,
+ message: "",
+ };
+ if (isDir) {
+ res.data = dummyfs[path];
+ return res;
+ }
+ res.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";
+ }
+ console.log("Open:", res);
+ return res;
+}
+async function openNode(node) {
+ return await open(node.path, node.is_dir);
+}
+main();
diff --git a/vfs/vfs.js b/vfs/vfs.js
new file mode 100644
index 0000000..440a8b5
--- /dev/null
+++ b/vfs/vfs.js
@@ -0,0 +1,202 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const CLASS_VFS_PATH = "wui_vfs_path";
+const CLASS_VFS_LIST = "wui_vfs_list";
+export class WuiVfs {
+ constructor(opts) {
+ this.opts = opts;
+ const el = document.getElementById(opts.id);
+ if (!el) {
+ console.error("WuiVfs: element id", opts.id, "not found");
+ return;
+ }
+ this.el = el;
+ this.com_path = new WuiVfsPath((path) => {
+ this.openDir(path);
+ });
+ this.el.appendChild(this.com_path.el);
+ this.com_list = new WuiVfsList((node) => {
+ this.openNode(node);
+ });
+ this.el.appendChild(this.com_list.el);
+ }
+ // filter the VFS list based on text value.
+ filter(text) {
+ this.com_list.filter(text);
+ }
+ // openNode is a handler that will be called when a node is clicked
+ // inside the WuiVfsList.
+ openNode(node) {
+ if (node.is_dir) {
+ this.openDir(node.path);
+ }
+ else {
+ this.opts.openNode(node);
+ }
+ }
+ // openDir is a handler that will be called when a path is clicked
+ // inside the WuiVfsPath.
+ async openDir(path) {
+ const res = await this.opts.open(path, true);
+ if (res.code != 200) {
+ return;
+ }
+ this.set(res.data);
+ }
+ set(node) {
+ if (node.is_dir) {
+ this.com_path.open(node);
+ this.com_list.open(node);
+ }
+ }
+}
+class WuiVfsList {
+ constructor(onClick) {
+ this.onClick = onClick;
+ this.node = null;
+ this.el = document.createElement("div");
+ this.el.classList.add(CLASS_VFS_LIST);
+ this.el.style.borderWidth = "1px";
+ this.el.style.borderStyle = "solid";
+ this.el.style.borderColor = "silver";
+ }
+ // filter re-render the list by including only the node that have name
+ // match with "text".
+ filter(text) {
+ const regexp = new RegExp(text, "i");
+ for (const elChild of this.el.children) {
+ if (regexp.test(elChild.innerHTML)) {
+ elChild.removeAttribute("hidden");
+ }
+ else {
+ elChild.setAttribute("hidden", "true");
+ }
+ }
+ }
+ open(node) {
+ this.node = node;
+ this.el.innerHTML = "";
+ if (!this.node) {
+ return;
+ }
+ if (!this.node.childs) {
+ return;
+ }
+ for (const c of this.node.childs) {
+ const el = document.createElement("div");
+ el.style.padding = "1em";
+ el.style.cursor = "pointer";
+ el.setAttribute("tabindex", "0");
+ el.innerText = c.name;
+ if (c.is_dir) {
+ el.innerText += "/";
+ el.style.backgroundColor = "cornsilk";
+ }
+ el.onclick = () => {
+ this.onClick(c);
+ };
+ el.onkeydown = (ev) => {
+ if (ev.key !== "Enter") {
+ return true;
+ }
+ this.onClick(c);
+ this.el.focus();
+ return false;
+ };
+ el.onblur = () => {
+ this.onBlur(c, el);
+ };
+ el.onmouseout = () => {
+ this.onBlur(c, el);
+ };
+ el.onfocus = () => {
+ this.onFocus(el);
+ };
+ el.onmouseover = () => {
+ this.onFocus(el);
+ };
+ this.el.appendChild(el);
+ }
+ }
+ onBlur(c, el) {
+ if (c.is_dir) {
+ el.style.backgroundColor = "cornsilk";
+ }
+ else {
+ el.style.backgroundColor = "white";
+ }
+ }
+ onFocus(el) {
+ el.style.backgroundColor = "aliceblue";
+ }
+}
+class WuiVfsPath {
+ constructor(onClick) {
+ this.el = document.createElement("div");
+ this.el.classList.add(CLASS_VFS_PATH);
+ this.el.style.borderWidth = "1px";
+ this.el.style.borderStyle = "solid";
+ this.el.style.borderColor = "silver";
+ this.el.style.overflow = "auto";
+ this.el.style.padding = "10px 10px 20px 0px";
+ this.el.style.whiteSpace = "nowrap";
+ this.onClick = onClick;
+ }
+ open(node) {
+ this.el.innerHTML = "";
+ let paths = [];
+ if (node.path == "/") {
+ paths.push(node.path);
+ }
+ else {
+ paths = node.path.split("/");
+ }
+ paths.forEach((path, x) => {
+ let fullPath = "";
+ let p = "";
+ if (x == 0) {
+ p = "/";
+ fullPath = "/";
+ }
+ else {
+ p = path;
+ fullPath = paths.slice(0, x + 1).join("/");
+ }
+ const crumb = document.createElement("span");
+ crumb.style.padding = "1em";
+ crumb.style.cursor = "pointer";
+ crumb.setAttribute("tabindex", "0");
+ crumb.innerHTML = p;
+ crumb.onclick = () => {
+ this.onClick(fullPath);
+ };
+ crumb.onkeydown = (ev) => {
+ if (ev.key !== "Enter") {
+ return true;
+ }
+ this.onClick(fullPath);
+ this.el.focus();
+ return false;
+ };
+ crumb.onmouseout = () => {
+ this.onBlur(crumb);
+ };
+ crumb.onblur = () => {
+ this.onBlur(crumb);
+ };
+ crumb.onmouseover = () => {
+ this.onFocus(crumb);
+ };
+ crumb.onfocus = () => {
+ this.onFocus(crumb);
+ };
+ this.el.appendChild(crumb);
+ });
+ }
+ onBlur(crumb) {
+ crumb.style.backgroundColor = "white";
+ }
+ onFocus(crumb) {
+ crumb.style.backgroundColor = "aliceblue";
+ }
+}
diff --git a/websocket_client.js b/websocket_client.js
new file mode 100644
index 0000000..3cfd1c7
--- /dev/null
+++ b/websocket_client.js
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+const AUTO_RECONNECT_INTERVAL = 5000;
+export class WuiWebSocketClient {
+ constructor(opts) {
+ this.opts = opts;
+ this.requestQueue = [];
+ this.reconnect_id = 0;
+ this.isOpen = false;
+ this.error = "";
+ this.address = opts.address;
+ if (opts.auto_reconnect) {
+ if (opts.auto_reconnect_interval <= 0) {
+ opts.auto_reconnect_interval = AUTO_RECONNECT_INTERVAL;
+ }
+ }
+ this.connect();
+ }
+ //
+ // Send the request and wait for response similar to HTTP
+ // request-response.
+ //
+ async Send(req) {
+ return new Promise((resolve) => {
+ const wuiRes = {
+ code: 0,
+ message: "",
+ };
+ const reqQueue = {
+ req: req,
+ cbSuccess: (res) => {
+ wuiRes.code = res.code;
+ wuiRes.message = res.message;
+ if (res.code === 200 && res.body.length > 0) {
+ wuiRes.data = JSON.parse(atob(res.body));
+ }
+ resolve(wuiRes);
+ },
+ cbFail: (err) => {
+ wuiRes.code = 500;
+ wuiRes.message = err;
+ resolve(wuiRes);
+ },
+ };
+ this.requestQueue.push(reqQueue);
+ this.conn.send(JSON.stringify(req));
+ });
+ }
+ connect() {
+ this.conn = new WebSocket(this.address);
+ this.conn.onclose = () => {
+ this.onClose();
+ };
+ this.conn.onerror = () => {
+ this.onError();
+ };
+ this.conn.onmessage = (ev) => {
+ this.onMessage(ev);
+ };
+ this.conn.onopen = () => {
+ this.onOpen();
+ };
+ }
+ // onClose handle connection closed by cleaning up the request
+ // queue.
+ onClose() {
+ this.requestQueue.forEach((reqq) => {
+ reqq.cbFail("connection closed");
+ });
+ this.isOpen = false;
+ this.error = "connection is closed by server";
+ if (this.opts.auto_reconnect && !this.reconnect_id) {
+ this.reconnect_id = setInterval(() => {
+ this.connect();
+ }, this.opts.auto_reconnect_interval);
+ }
+ if (this.opts.onDisconnected) {
+ this.opts.onDisconnected();
+ }
+ }
+ onError() {
+ if (this.opts.onError) {
+ this.opts.onError();
+ }
+ }
+ onMessage(ev) {
+ const res = JSON.parse(ev.data);
+ this.requestQueue.forEach((reqq, x) => {
+ if (reqq.req.id === res.id) {
+ reqq.cbSuccess(res);
+ this.requestQueue.splice(x, 1);
+ return;
+ }
+ });
+ if (this.opts.onBroadcast && res.id == 0) {
+ this.opts.onBroadcast(res);
+ }
+ }
+ onOpen() {
+ this.isOpen = true;
+ this.error = "";
+ if (this.reconnect_id) {
+ clearInterval(this.reconnect_id);
+ this.reconnect_id = 0;
+ }
+ if (this.opts.onConnected) {
+ this.opts.onConnected();
+ }
+ }
+}