diff options
| -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(); + } + } +} |
