diff options
| author | Shulhan <ms@kilabit.info> | 2024-09-15 14:32:48 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-09-15 14:32:48 +0700 |
| commit | d1e96e09438b4a5c7580b86c469e817a61be991f (patch) | |
| tree | 4d81fb2fd62207bbb9162b81083c721ec8fd8e29 | |
| parent | 1cc9c9dd68a3a59c685505228336430624608852 (diff) | |
| download | pakakeh.ts-d1e96e09438b4a5c7580b86c469e817a61be991f.tar.xz | |
all: commit all generate JavaScript files
This is to simplify development on third party where they can
clone and include the file directly without installing or running
anything to build the files.
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | editor/editor.js | 205 | ||||
| -rw-r--r-- | editor/example.js | 65 | ||||
| -rw-r--r-- | input/checkboxes.js | 137 | ||||
| -rw-r--r-- | input/example.js | 224 | ||||
| -rw-r--r-- | input/file.js | 104 | ||||
| -rw-r--r-- | input/number.js | 170 | ||||
| -rw-r--r-- | input/option.js | 3 | ||||
| -rw-r--r-- | input/select.js | 152 | ||||
| -rw-r--r-- | input/string.js | 130 | ||||
| -rw-r--r-- | notif/example.js | 42 | ||||
| -rw-r--r-- | notif/notif.js | 64 | ||||
| -rw-r--r-- | response.js | 3 | ||||
| -rw-r--r-- | vfs/example.js | 145 | ||||
| -rw-r--r-- | vfs/vfs.js | 202 | ||||
| -rw-r--r-- | websocket_client.js | 110 |
16 files changed, 1758 insertions, 2 deletions
@@ -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 = " ℹ"; + 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 = " ℹ"; + 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 = " ℹ"; + 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 = " ℹ"; + 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 = " ℹ"; + 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(); + } + } +} |
