From 119cd6e0af2e37822f0a635cf53429c5f3521422 Mon Sep 17 00:00:00 2001
From: Miroslav Popov <info@forexsb.com>
Date: Sun, 21 Apr 2024 19:41:20 +0300
Subject: [PATCH] Major project reorganization.

---
 .gitignore         |   2 +
 HttpRequest.ts     | 122 ----------------------------
 README.md          |  81 ++++++++++++++-----
 index.html         | 110 ++++++++++++++++++++++++++
 index.js           | 193 +++++++++++++++++++++++++++++++++++++++++++++
 index.js.map       |   1 +
 index.tsbuildinfo  |   1 +
 lib/Application.ts |  99 +++++++++++++++++++++++
 lib/HttpRequest.ts | 144 +++++++++++++++++++++++++++++++++
 package-lock.json  |  28 +++++++
 package.json       |  17 ++++
 tsconfig.json      |  13 +++
 12 files changed, 670 insertions(+), 141 deletions(-)
 create mode 100644 .gitignore
 delete mode 100644 HttpRequest.ts
 create mode 100644 index.html
 create mode 100644 index.js
 create mode 100644 index.js.map
 create mode 100644 index.tsbuildinfo
 create mode 100644 lib/Application.ts
 create mode 100644 lib/HttpRequest.ts
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 tsconfig.json

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb79dd5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.idea
diff --git a/HttpRequest.ts b/HttpRequest.ts
deleted file mode 100644
index 11a6de5..0000000
--- a/HttpRequest.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * HttpRequest
- *
- * A simple XMLHttpRequest helper
- * https://github.com/PopovMP/http-request
- *
- * Copyright @ 2022 Miroslav Popov
- *
- * v1.1 2023.06.11
- */
-
-interface HttpRequestOptions {
-    headers     : object
-    responseType: XMLHttpRequestResponseType
-}
-
-interface HttpRequestResponse {
-    readyState  : number
-    response    : ArrayBuffer | Blob | Document | Object | string | undefined
-    responseText: string | undefined
-    responseType: XMLHttpRequestResponseType
-    responseURL : string
-    status      : number
-    statusText  : string
-    headers     : Record<string, string>
-}
-
-type HttpRequestCallback = (res: HttpRequestResponse) => void
-
-/**
- * Provides get and post methods
- * @class HttpRequest
- */
-class HttpRequest {
-
-    /**
-     * Make a GET request
-     */
-    public static get(url: string, options: HttpRequestOptions, callback: HttpRequestCallback): void {
-        HttpRequest.request("GET", url, null, options, callback);
-    }
-
-    /**
-     * Make a POST request
-     */
-    public static post(url: string, body: any, options: HttpRequestOptions, callback: HttpRequestCallback): void {
-        HttpRequest.request("POST", url, body, options, callback);
-    }
-
-    /**
-     * Make a request
-     */
-    private static request(method: string, url: string, body: any, options: HttpRequestOptions,
-        callback: HttpRequestCallback): void {
-        const req: XMLHttpRequest = new XMLHttpRequest();
-
-        req.open(method, url, true);
-
-        if (typeof options.headers === "object") {
-            for (const name of Object.keys(options.headers)) {
-                // @ts-ignore
-                req.setRequestHeader(name, options.headers[name]);
-            }
-        }
-
-        if (typeof options.responseType === "string") {
-            req.responseType = options.responseType;
-        }
-
-        req.onreadystatechange = HttpRequest.req_readyStateChange.bind(this, req, callback);
-        req.onerror            = HttpRequest.req_error.bind(this, callback);
-        req.send(body);
-    }
-
-    /**
-     * Handles XMLHttpRequest :: onreadystatechange
-     * Calls the callback when the XMLHttpRequest is DONE
-     */
-    static req_readyStateChange(req: XMLHttpRequest, callback: HttpRequestCallback): void {
-        if (req.readyState !== XMLHttpRequest.DONE) {
-            return;
-        }
-
-        const headers: {[header: string]: string} = {};
-        const reqResponseHeaders: string[] = req.getAllResponseHeaders().trim().split(/[\r\n]+/);
-        for (const header of reqResponseHeaders) {
-            const parts: string[] = header.split(": ");
-            // @ts-ignore
-            headers[parts[0]] = parts[1];
-        }
-
-        const isResponseText: boolean = req.responseType === "text" || req.responseType === "";
-
-        callback({
-            readyState  : req.readyState,
-            response    : isResponseText ? undefined : req.response,
-            responseText: isResponseText ? req.responseText : undefined,
-            responseType: req.responseType,
-            responseURL : req.responseURL,
-            status      : req.status,
-            statusText  : req.statusText,
-            headers     : headers,
-        });
-    }
-
-    /**
-     * Handles XMLHttpRequest :: onerror
-     * Calls the callback when the XMLHttpRequest rises an error
-     */
-    static req_error(callback: HttpRequestCallback): void {
-        callback({
-            readyState  : 4,
-            response    : undefined,
-            responseText: undefined,
-            responseType: "",
-            responseURL : "",
-            status      : 400,
-            statusText  : "An error occurred during the transaction",
-            headers     : {},
-        });
-    }
-}
diff --git a/README.md b/README.md
index d2b9594..81e907d 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,77 @@
 # HttpRequest
 
-Simple HTTP Request helper for the browser
+Simple HTTP Request helper for the browser.
 
-## Get JSON
+HttpRequest provides three methods: `get`, `post`, and `form`.
+
+## Acquire
+
+Get the `HttpRequest` class in TypeScript from the `lib` subdirectory.
+
+Get `HttpRequest` compiled to a JavaScript class from the `application.js` file.
+
+
+## Tests
+
+See examples and tests: https://popovmp.github.io/http-request/
+
+## Get Text
 
 ```TypeScript
-const url      = "https://example.com/foo.json";
-const oprtions = {responseType: "json"};
+const url:string = "https://httpbin.org/get";
+const options: HttpRequestOptions = {};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    console.log(JSON.stringify(res));
+});
+```
 
-HttpRequest.get(url, options, (res: HttpRequestResponse) => {
-	const foo = res.response;
-})
+## Get Json
+
+```TypeScript
+const url:string = "https://httpbin.org/get";
+const options: HttpRequestOptions = {responseType: "json"};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    console.log(JSON.stringify(res));
+});
 ```
 
+
 ## Get binary data
 
 ```TypeScript
 const url     = "https://exmaple.com/buffer.bin";
-const options = {
-	headers     : {},
-	responseType: "arraybuffer" as XMLHttpRequestResponseType,
-};
+const options = {headers: {}, responseType: "arraybuffer"};
 
 HttpRequest.get(url, options, (res: HttpRequestResponse) => {
-	if ( (res.status === 200 || res.status === 304) && res.response !== undefined) {
-		const buffer: ArrayBuffer = res.response;
-		// do somethign with the buffer
-	}
-	else {
-		console.error(`Status: ${res.status}`);
-	}
-})
+    if ((res.status === 200 || res.status === 304) && res.response !== undefined) {
+        const buffer: ArrayBuffer = res.response;
+        // Do somethign with the buffer
+    }
+    else {
+        console.error(`Status: ${res.status}`);
+    }
+});
+```
+
+## Post JSON
+
+```TypeScript
+const url     = "https://exmaple.com/api/user";
+const body    = {username: "John", email: "john@example.com", pasword: "12343"};
+const options = {headers: {}, responseType: "json"};
+
+HttpRequest.post(url, body, options, (res: HttpRequestResponse) => {
+    const user: User = res.response;
+});
+```
+
+## Post Form
+
+```TypeScript
+        const url: string = "https://httpbin.org/post";
+const options: HttpRequestOptions = {responseType: "json"};
+const form: {[param: string]: string|number} = {foo: "bar", baz: 42};
+HttpRequest.form(url, form, options, (res: HttpRequestResponse): void => {
+    console.log(JSON.stringify(res));
+});
 ```
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f7acf44
--- /dev/null
+++ b/index.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>HttpRequest Lib</title>
+</head>
+<body>
+
+<h1>HttpRequest Test Page</h1>
+
+<p>Test requests against <a href="https://httpbin.org/">https://httpbin.org/</a></p>
+
+<h2>GET Text</h2>
+<pre>
+const url:string = "https://httpbin.org/get";
+const options: HttpRequestOptions = {};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="get-text-btn">GET JSON</button>
+<pre id="get-text-res"></pre>
+
+
+<h2>GET JSON</h2>
+<pre>
+const url:string = "https://httpbin.org/get";
+const options: HttpRequestOptions = {responseType: "json"};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="get-json-btn">GET JSON</button>
+<pre id="get-json-res"></pre>
+
+
+<h2>POST JSON Text</h2>
+<h3>Post JSON object as a string</h3>
+<pre>
+const url: string = "https://httpbin.org/post";
+const options: HttpRequestOptions = {responseType: "json", headers: {"X-Custom": "custom"}};
+const body: any = {foo: "bar", baz: 42};
+const bodyTxt: string = JSON.stringify(body);
+HttpRequest.post(url, bodyTxt, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="post-json-text-btn">POST JSON</button>
+<pre id="post-json-text-res"></pre>
+
+
+<h2>POST JSON</h2>
+<h3>Post JSON</h3>
+<pre>
+const url: string = "https://httpbin.org/anything";
+const options: HttpRequestOptions = {responseType: "json", headers: {"Content-Type": "application/json"}};
+const body: any = {foo: "bar", baz: 42};
+HttpRequest.post(url, body, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="post-json-btn">POST JSON</button>
+<pre id="post-json-res"></pre>
+
+
+<h2>POST Form</h2>
+<h3>Post form</h3>
+<pre>
+const url: string = "https://httpbin.org/post";
+const options: HttpRequestOptions = {responseType: "json"};
+const form: {[param: string]: string|number} = {foo: "bar", baz: 42};
+HttpRequest.form(url, form, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="post-form-btn">POST Form</button>
+<pre id="post-form-res"></pre>
+
+
+<h2>Timeout</h2>
+<pre>
+const url: string = "https://httpbin.org/delay/10";
+const options: HttpRequestOptions = {timeout: 5 * 1000};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="post-timeout-btn">POST Form</button>
+<pre id="post-timeout-res"></pre>
+
+
+<h2>Resources not found (404)</h2>
+<pre>
+const url: string = "https://httpbin.org/status/404";
+const options: HttpRequestOptions = {};
+HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+    resField.innerText = JSON.stringify(res, null, 2);
+});
+</pre>
+<button type="button" id="post-404-btn">POST Form</button>
+<pre id="post-404-res"></pre>
+
+
+<script src="index.js"></script>
+<script>
+    const app = new Application();
+    app.initialize();
+</script>
+</body>
+</html>
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..a57f3cb
--- /dev/null
+++ b/index.js
@@ -0,0 +1,193 @@
+"use strict";
+class Application {
+    initialize() {
+        document.getElementById("get-text-btn")
+            .addEventListener("click", this.getTextTest.bind(this));
+        document.getElementById("get-json-btn")
+            .addEventListener("click", this.getJsonTest.bind(this));
+        document.getElementById("post-json-text-btn")
+            .addEventListener("click", this.postJsonTextTest.bind(this));
+        document.getElementById("post-json-btn")
+            .addEventListener("click", this.postJsonTest.bind(this));
+        document.getElementById("post-form-btn")
+            .addEventListener("click", this.postFormTest.bind(this));
+        document.getElementById("post-timeout-btn")
+            .addEventListener("click", this.postTimeoutTest.bind(this));
+        document.getElementById("post-404-btn")
+            .addEventListener("click", this.post404Test.bind(this));
+    }
+    getTextTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("get-text-res");
+        const url = "https://httpbin.org/get";
+        const options = {};
+        HttpRequest.get(url, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    getJsonTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("get-json-res");
+        const url = "https://httpbin.org/get";
+        const options = { responseType: "json" };
+        HttpRequest.get(url, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    postJsonTextTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("post-json-text-res");
+        const url = "https://httpbin.org/post";
+        const options = { responseType: "json", headers: { "X-Custom": "custom" } };
+        const body = { foo: "bar", baz: 42 };
+        const bodyTxt = JSON.stringify(body);
+        HttpRequest.post(url, bodyTxt, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    postJsonTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("post-json-res");
+        const url = "https://httpbin.org/anything";
+        const options = { responseType: "json", headers: { "Content-Type": "application/json" } };
+        const body = { foo: "bar", baz: 42 };
+        HttpRequest.post(url, body, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    postFormTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("post-form-res");
+        const url = "https://httpbin.org/post";
+        const options = { responseType: "json" };
+        const form = { foo: "bar", baz: 42 };
+        HttpRequest.form(url, form, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    postTimeoutTest(event) {
+        event.preventDefault();
+        const resField = document.getElementById("post-timeout-res");
+        const url = "https://httpbin.org/delay/10";
+        const options = { timeout: 5 * 1000 };
+        HttpRequest.get(url, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+    post404Test(event) {
+        event.preventDefault();
+        const resField = document.getElementById("post-404-res");
+        const url = "https://httpbin.org/status/404";
+        const options = {};
+        HttpRequest.get(url, options, (res) => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+}
+/**
+ * HttpRequest
+ *
+ * A simple XMLHttpRequest helper
+ * https://github.com/PopovMP/http-request
+ *
+ * Copyright @ 2024 Miroslav Popov
+ *
+ * v1.4 2024.04.21
+ */
+/**
+ * Provides `get` and `post` methods
+ * @class HttpRequest
+ */
+class HttpRequest {
+    /**
+     * Make a GET request
+     */
+    static get(url, options, callback) {
+        HttpRequest.request("GET", url, null, options, callback);
+    }
+    /**
+     * Make a POST request
+     */
+    static post(url, body, options, callback) {
+        HttpRequest.request("POST", url, body, options, callback);
+    }
+    /**
+     * Make POST request encoded as "application/x-www-form-urlencoded"
+     */
+    static form(url, formData, options, callback) {
+        const parameters = [];
+        for (const param of Object.keys(formData))
+            parameters.push(`${param}=${encodeURIComponent(formData[param])}`);
+        const body = parameters.join("&");
+        if (!options.headers)
+            options.headers = {};
+        options.headers["Content-Type"] = "application/x-www-form-urlencoded";
+        HttpRequest.request("POST", url, body, options, callback);
+    }
+    /**
+     * Make a request
+     */
+    static request(method, url, body, options, callback) {
+        let isCompleted = false;
+        const req = new XMLHttpRequest();
+        req.open(method, url, true);
+        if (typeof options.headers === "object") {
+            for (const name of Object.keys(options.headers))
+                req.setRequestHeader(name, options.headers[name]);
+        }
+        req.timeout = typeof options.timeout === "number" ? options.timeout : 20 * 1000;
+        req.responseType = options.responseType || "";
+        req.onreadystatechange = req_readyStateChange;
+        req.onerror = req_error;
+        req.ontimeout = req_timeout;
+        req.onabort = req_abort;
+        req.send(body);
+        function req_readyStateChange() {
+            if (req.readyState !== XMLHttpRequest.DONE || req.status === 0)
+                return;
+            if (isCompleted)
+                return;
+            const headers = {};
+            const resHeaders = req.getAllResponseHeaders().trim().split(/[\r\n]+/);
+            for (const header of resHeaders) {
+                const parts = header.split(": ");
+                headers[parts[0]] = parts[1];
+            }
+            const isResponseText = req.responseType === "text" || req.responseType === "";
+            isCompleted = true;
+            callback({
+                response: isResponseText ? undefined : req.response,
+                responseText: isResponseText ? req.responseText : undefined,
+                responseType: req.responseType,
+                responseURL: req.responseURL,
+                status: req.status,
+                statusText: req.statusText,
+                headers: headers,
+            });
+        }
+        function req_abort() {
+            resError("Request aborted");
+        }
+        function req_timeout() {
+            resError("Request timeout");
+        }
+        function req_error() {
+            resError("An error occurred during the transaction");
+        }
+        function resError(message) {
+            if (isCompleted)
+                return;
+            isCompleted = true;
+            callback({
+                response: undefined,
+                responseText: undefined,
+                responseType: req.responseType,
+                responseURL: url,
+                status: req.status,
+                statusText: message,
+                headers: {},
+            });
+        }
+    }
+}
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/index.js.map b/index.js.map
new file mode 100644
index 0000000..67f38d9
--- /dev/null
+++ b/index.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["lib/Application.ts","lib/HttpRequest.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/index.tsbuildinfo b/index.tsbuildinfo
new file mode 100644
index 0000000..1407f3c
--- /dev/null
+++ b/index.tsbuildinfo
@@ -0,0 +1 @@
+{"bundle":{"commonSourceDirectory":"./lib","sourceFiles":["./lib/Application.ts","./lib/HttpRequest.ts"],"js":{"sections":[{"pos":0,"end":13,"kind":"prologue","data":"use strict"},{"pos":14,"end":7202,"kind":"text"}],"sources":{"prologues":[{"file":0,"text":"","directives":[{"pos":-1,"end":-1,"expression":{"pos":-1,"end":-1,"text":"use strict"}}]}]},"mapHash":"988bcaea8df7201dc98cc185d7d5754fc42fab9d9b06c6ab682cef0c3f49b3df","hash":"8d6c5e3e22301c10ac79710470f473ba2a5cce64d82cbf5720d30adeab30c424"}},"program":{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./lib/application.ts","./lib/httprequest.ts"],"fileInfos":["824cb491a40f7e8fdeb56f1df5edf91b23f3e3ee6b4cde84d4a99be32338faee","45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","5514e54f17d6d74ecefedc73c504eadffdeda79c7ea205cf9febead32d45c4bc","87d693a4920d794a73384b3c779cadcb8548ac6945aa7a925832fe2418c9527a","138fb588d26538783b78d1e3b2c2cc12d55840b97bf5e08bca7f7a174fbe2f17","dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","ea011c76963fb15ef1cdd7ce6a6808b46322c527de2077b6cfdf23ae6f5f9ec7","38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","b20fe0eca9a4e405f1a5ae24a2b3290b37cf7f21eba6cbe4fc3fab979237d4f3","811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","49ed889be54031e1044af0ad2c603d627b8bda8b50c1a68435fe85583901d072","e93d098658ce4f0c8a0779e6cab91d0259efb88a318137f686ad76f8410ca270","063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","5e07ed3809d48205d5b985642a59f2eba47c402374a7cf8006b686f79efadcbd","2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","8073890e29d2f46fdbc19b8d6d2eb9ea58db9a2052f8640af20baff9afbc8640","368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","51e547984877a62227042850456de71a5c45e7fe86b7c975c6e68896c86fa23b","956d27abdea9652e8368ce029bb1e0b9174e9678a273529f426df4b3d90abd60","4fa6ed14e98aa80b91f61b9805c653ee82af3502dc21c9da5268d3857772ca05","e6633e05da3ff36e6da2ec170d0d03ccf33de50ca4dc6f5aeecb572cedd162fb","d8670852241d4c6e03f2b89d67497a4bbefe29ecaa5a444e2c11a9b05e6fccc6","8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","caccc56c72713969e1cfe5c3d44e5bab151544d9d2b373d7dbe5a1e4166652be","3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","50d53ccd31f6667aff66e3d62adf948879a3a16f05d89882d1188084ee415bbc","33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","23a55a365892166010e710a0d48b253bf8b4031a68d5d18bfdd461ca9955db5b","bb73ff13acd5e322caf39bbd813546674150e136994949b5a611adbf47af28c1"],"root":[59,60],"options":{"outFile":"./index.js","sourceMap":true,"strict":true,"target":2}},"version":"5.4.5"}
\ No newline at end of file
diff --git a/lib/Application.ts b/lib/Application.ts
new file mode 100644
index 0000000..9a76b72
--- /dev/null
+++ b/lib/Application.ts
@@ -0,0 +1,99 @@
+class Application {
+    public initialize(): void {
+        (document.getElementById("get-text-btn") as HTMLElement)
+            .addEventListener("click", this.getTextTest.bind(this));
+        (document.getElementById("get-json-btn") as HTMLElement)
+            .addEventListener("click", this.getJsonTest.bind(this));
+        (document.getElementById("post-json-text-btn") as HTMLElement)
+            .addEventListener("click", this.postJsonTextTest.bind(this));
+        (document.getElementById("post-json-btn") as HTMLElement)
+            .addEventListener("click", this.postJsonTest.bind(this));
+        (document.getElementById("post-form-btn") as HTMLElement)
+            .addEventListener("click", this.postFormTest.bind(this));
+        (document.getElementById("post-timeout-btn") as HTMLElement)
+            .addEventListener("click", this.postTimeoutTest.bind(this));
+        (document.getElementById("post-404-btn") as HTMLElement)
+            .addEventListener("click", this.post404Test.bind(this));
+    }
+
+    public getTextTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("get-text-res") as HTMLElement;
+
+        const url:string = "https://httpbin.org/get";
+        const options: HttpRequestOptions = {};
+        HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public getJsonTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("get-json-res") as HTMLElement;
+
+        const url:string = "https://httpbin.org/get";
+        const options: HttpRequestOptions = {responseType: "json"};
+        HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public postJsonTextTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("post-json-text-res") as HTMLElement;
+
+        const url: string = "https://httpbin.org/post";
+        const options: HttpRequestOptions = {responseType: "json", headers: {"X-Custom": "custom"}};
+        const body: any = {foo: "bar", baz: 42};
+        const bodyTxt: string = JSON.stringify(body);
+        HttpRequest.post(url, bodyTxt, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public postJsonTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("post-json-res") as HTMLElement;
+
+        const url: string = "https://httpbin.org/anything";
+        const options: HttpRequestOptions = {responseType: "json", headers: {"Content-Type": "application/json"}};
+        const body: any = {foo: "bar", baz: 42};
+        HttpRequest.post(url, body, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public postFormTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("post-form-res") as HTMLElement;
+
+        const url: string = "https://httpbin.org/post";
+        const options: HttpRequestOptions = {responseType: "json"};
+        const form: {[param: string]: string|number} = {foo: "bar", baz: 42};
+        HttpRequest.form(url, form, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public postTimeoutTest(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("post-timeout-res") as HTMLElement;
+
+        const url: string = "https://httpbin.org/delay/10";
+        const options: HttpRequestOptions = {timeout: 5 * 1000};
+        HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+
+    public post404Test(event: Event): void {
+        event.preventDefault();
+        const resField: HTMLElement = document.getElementById("post-404-res") as HTMLElement;
+
+        const url: string = "https://httpbin.org/status/404";
+        const options: HttpRequestOptions = {};
+        HttpRequest.get(url, options, (res: HttpRequestResponse): void => {
+            resField.innerText = JSON.stringify(res, null, 2);
+        });
+    }
+}
diff --git a/lib/HttpRequest.ts b/lib/HttpRequest.ts
new file mode 100644
index 0000000..194e135
--- /dev/null
+++ b/lib/HttpRequest.ts
@@ -0,0 +1,144 @@
+/**
+ * HttpRequest
+ *
+ * A simple XMLHttpRequest helper
+ * https://github.com/PopovMP/http-request
+ *
+ * Copyright @ 2024 Miroslav Popov
+ *
+ * v1.4 2024.04.21
+ */
+
+interface HttpRequestOptions {
+    headers     ?: Record<string, string>
+    responseType?: "" | "arraybuffer" | "blob" | "document" | "json" | "text"
+    timeout     ?: number // a number of milliseconds
+}
+
+interface HttpRequestResponse {
+    response    : ArrayBuffer | Blob | Document | Object | string | undefined
+    responseText: string | undefined
+    responseType: "" | "arraybuffer" | "blob" | "document" | "json" | "text"
+    responseURL : string
+    status      : number
+    statusText  : string
+    headers     : Record<string, string>
+}
+
+type HttpRequestMethod   = "GET" | "POST" | "HEAD" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"
+type HttpRequestCallback = (res: HttpRequestResponse) => void
+
+/**
+ * Provides `get` and `post` methods
+ * @class HttpRequest
+ */
+class HttpRequest {
+
+    /**
+     * Make a GET request
+     */
+    public static get(url: string, options: HttpRequestOptions, callback: HttpRequestCallback): void {
+        HttpRequest.request("GET", url, null, options, callback);
+    }
+
+    /**
+     * Make a POST request
+     */
+    public static post(url: string, body: any, options: HttpRequestOptions,
+                       callback: HttpRequestCallback): void {
+        HttpRequest.request("POST", url, body, options, callback);
+    }
+
+    /**
+     * Make POST request encoded as "application/x-www-form-urlencoded"
+     */
+    public static form(url: string, formData: Record<string, string|number>,
+                  options: HttpRequestOptions, callback: HttpRequestCallback): void {
+
+        const parameters: string[] = [];
+        for (const param of Object.keys(formData))
+            parameters.push(`${param}=${encodeURIComponent(formData[param])}`);
+        const body: string = parameters.join("&");
+
+        if (!options.headers)
+            options.headers = {};
+        options.headers["Content-Type"] = "application/x-www-form-urlencoded";
+
+        HttpRequest.request("POST", url, body, options, callback);
+    }
+
+    /**
+     * Make a request
+     */
+    public static request(method: HttpRequestMethod, url: string, body: any,
+                          options: HttpRequestOptions, callback: HttpRequestCallback): void {
+        let isCompleted: boolean = false;
+
+        const req: XMLHttpRequest = new XMLHttpRequest();
+        req.open(method, url, true);
+
+        if (typeof options.headers === "object") {
+            for (const name of Object.keys(options.headers))
+                req.setRequestHeader(name, options.headers[name]);
+        }
+
+        req.timeout            = typeof options.timeout === "number" ? options.timeout : 20 * 1000;
+        req.responseType       = options.responseType || "";
+        req.onreadystatechange = req_readyStateChange;
+        req.onerror            = req_error;
+        req.ontimeout          = req_timeout;
+        req.onabort            = req_abort;
+        req.send(body);
+
+        function req_readyStateChange(): void {
+            if (req.readyState !== XMLHttpRequest.DONE || req.status === 0) return;
+            if (isCompleted) return;
+
+            const headers   : Record<string, string> = {};
+            const resHeaders: string[] = req.getAllResponseHeaders().trim().split(/[\r\n]+/);
+            for (const header of resHeaders) {
+                const parts: string[] = header.split(": ");
+                headers[parts[0]] = parts[1];
+            }
+
+            const isResponseText: boolean = req.responseType === "text" || req.responseType === "";
+
+            isCompleted = true;
+            callback({
+                 response    : isResponseText ? undefined : req.response,
+                 responseText: isResponseText ? req.responseText : undefined,
+                 responseType: req.responseType,
+                 responseURL : req.responseURL,
+                 status      : req.status,
+                 statusText  : req.statusText,
+                 headers     : headers,
+            });
+        }
+
+        function req_abort(): void {
+            resError("Request aborted");
+        }
+
+        function req_timeout(): void {
+            resError("Request timeout");
+        }
+
+        function req_error(): void {
+            resError("An error occurred during the transaction");
+        }
+
+        function resError(message: string): void {
+            if (isCompleted) return;
+            isCompleted = true;
+            callback({
+                 response    : undefined,
+                 responseText: undefined,
+                 responseType: req.responseType,
+                 responseURL : url,
+                 status      : req.status,
+                 statusText  : message,
+                 headers     : {},
+             });
+        }
+    }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0f29a65
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,28 @@
+{
+  "name": "http-request",
+  "version": "1.4.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "http-request",
+      "version": "1.4.0",
+      "license": "MIT",
+      "dependencies": {
+        "typescript": "^5.4.5"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.4.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+      "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..69f05eb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "http-request",
+  "version": "1.4.0",
+  "description": "Simple HTTP Request helper for the browser",
+  "main": "index.js",
+  "directories": {
+    "lib": "lib"
+  },
+  "dependencies": {
+    "typescript": "^5.4.5"
+  },
+  "scripts": {
+    "build": "tsc -p tsconfig.json"
+  },
+  "author": "Popov",
+  "license": "MIT"
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..9b13238
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+  "compilerOptions": {
+    "incremental": true,
+    "lib": ["ES2022", "DOM"],
+    "target": "ES6",
+    "strict": true,
+    "outFile": "./index.js",
+    "sourceMap": true
+  },
+  "include": [
+    "./lib/*.ts"
+  ]
+}