From b167ae569626b853458791dd687abd9db5dd8b8f Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Thu, 2 May 2024 22:54:40 +0000 Subject: [PATCH] Went through todos --- Message/Request/Request.js | 26 +- Message/Response/ResponseManager.js | 4 +- Model/Action/Dom/AddDOM.js | 4 +- Model/Action/Dom/Click.js | 4 +- Model/Action/Dom/Datalist/Append.js | 4 +- Model/Action/Dom/Datalist/Clear.js | 4 +- Model/Action/Dom/Focus.js | 4 +- Model/Action/Dom/FormSubmit.js | 4 +- Model/Action/Dom/Hide.js | 4 +- Model/Action/Dom/Popup.js | 4 +- Model/Action/Dom/Redirect.js | 10 +- Model/Action/Dom/Reload.js | 30 -- Model/Action/Dom/Remove.js | 4 +- Model/Action/Dom/RemoveDOM.js | 4 +- Model/Action/Dom/RemoveValue.js | 4 +- Model/Action/Dom/Show.js | 4 +- Model/Action/Dom/Table/Append.js | 4 +- Model/Action/Dom/Table/Clear.js | 4 +- Model/Action/Event/If.js | 4 +- Model/Action/Event/Prevent.js | 4 +- Model/Action/Message/Log.js | 10 +- Model/Action/Message/Request.js | 6 +- UI/Component/AdvancedCalendar.js | 0 UI/Component/AdvancedRange.js | 0 UI/Component/CodeArea.js | 113 ++++++ UI/Component/Form.js | 2 +- UI/Component/Input.js | 139 -------- UI/Component/SmartTextInput.js | 329 ++++++++++++++++++ .../{AdvancedInput.js => TagInput.js} | 24 +- UI/GeneralUI.js | 25 +- UI/Input/Keyboard/KeyboardManager.js | 7 +- Uri/UriFactory.js | 20 +- Utils/Debug.js | 113 ------ Views/FormView.js | 38 +- 34 files changed, 588 insertions(+), 372 deletions(-) delete mode 100755 Model/Action/Dom/Reload.js delete mode 100755 UI/Component/AdvancedCalendar.js delete mode 100755 UI/Component/AdvancedRange.js create mode 100644 UI/Component/CodeArea.js delete mode 100755 UI/Component/Input.js create mode 100644 UI/Component/SmartTextInput.js rename UI/Component/{AdvancedInput.js => TagInput.js} (95%) mode change 100755 => 100644 delete mode 100755 Utils/Debug.js diff --git a/Message/Request/Request.js b/Message/Request/Request.js index c2acc84..b649b2c 100755 --- a/Message/Request/Request.js +++ b/Message/Request/Request.js @@ -239,6 +239,11 @@ export class Request this.data = data; }; + addData(name, data) + { + this.data[name] = data; + }; + /** * Get request data. * @@ -316,24 +321,17 @@ export class Request // @question Consider to change to fetch if (this.xhr.readyState !== 1) { - if (this.type === RequestType.FORM_DATA) { - // GET request doesn't allow body/payload. Therefor we have to put the data into the uri - if (this.method === RequestMethod.GET) { - let url = this.uri; - for (const pair of this.data.entries()) { - url += '&' + pair[0] + '=' + pair[1]; - } - - this.xhr.open(this.method, UriFactory.build(url)); - } else { - this.xhr.open(this.method, UriFactory.build(this.uri)); + let url = this.uri; + if (this.method === RequestMethod.GET) { + for (const pair of Object.entries(this.data)) { + url += '&' + pair[0] + '=' + pair[1]; } } else { - console.log(UriFactory.build(this.uri)); - - this.xhr.open(this.method, UriFactory.build(this.uri)); + url = this.uri; } + this.xhr.open(this.method, UriFactory.build(url)); + for (const p in this.requestHeader) { if (Object.prototype.hasOwnProperty.call(this.requestHeader, p) && this.requestHeader[p] !== '') { if (this.requestHeader[p] !== 'multipart/form-data') { diff --git a/Message/Response/ResponseManager.js b/Message/Response/ResponseManager.js index 17b49c4..35e5935 100755 --- a/Message/Response/ResponseManager.js +++ b/Message/Response/ResponseManager.js @@ -1,4 +1,4 @@ -import { jsOMS } from '../../Utils/oLib.js'; +import { Logger } from '../../Log/Logger.js'; /** * @typedef {import('../Request/Request.js').Request} Request */ @@ -72,7 +72,7 @@ export class ResponseManager } else if (typeof this.messages[key] !== 'undefined') { this.messages[key].null(data); } else { - jsOMS.Log.Logger.instance.warning('Undefined type: ' + key); + Logger.instance.warning('Undefined type: ' + key); } }; }; diff --git a/Model/Action/Dom/AddDOM.js b/Model/Action/Dom/AddDOM.js index 418057f..1bad3af 100644 --- a/Model/Action/Dom/AddDOM.js +++ b/Model/Action/Dom/AddDOM.js @@ -21,5 +21,7 @@ export function domAddElement (action, callback, id) e.appendChild.removeChild(i); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Click.js b/Model/Action/Dom/Click.js index 311529d..b8ec9ed 100755 --- a/Model/Action/Dom/Click.js +++ b/Model/Action/Dom/Click.js @@ -25,5 +25,7 @@ export function domClickAction (action, callback, id) i.click(); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Datalist/Append.js b/Model/Action/Dom/Datalist/Append.js index 5f098d3..04a94be 100755 --- a/Model/Action/Dom/Datalist/Append.js +++ b/Model/Action/Dom/Datalist/Append.js @@ -22,5 +22,7 @@ export function datalistAppend (action, callback) datalist.appendChild(option); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Datalist/Clear.js b/Model/Action/Dom/Datalist/Clear.js index bd0bba2..a1fc0fa 100755 --- a/Model/Action/Dom/Datalist/Clear.js +++ b/Model/Action/Dom/Datalist/Clear.js @@ -16,5 +16,7 @@ export function datalistClear (action, callback) e.removeChild(e.firstChild); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Focus.js b/Model/Action/Dom/Focus.js index f1a5daf..f571967 100755 --- a/Model/Action/Dom/Focus.js +++ b/Model/Action/Dom/Focus.js @@ -18,5 +18,7 @@ export function focusAction (action, callback) focus.focus(); - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/FormSubmit.js b/Model/Action/Dom/FormSubmit.js index 020d36a..7e687eb 100755 --- a/Model/Action/Dom/FormSubmit.js +++ b/Model/Action/Dom/FormSubmit.js @@ -29,5 +29,7 @@ export function formSubmitAction (action, callback, id) formManager.submit(formManager.get(i.id)); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Hide.js b/Model/Action/Dom/Hide.js index 69a948d..05de02b 100755 --- a/Model/Action/Dom/Hide.js +++ b/Model/Action/Dom/Hide.js @@ -20,5 +20,7 @@ export function hideAction (action, callback) /** global: jsOMS */ jsOMS.addClass(hide, 'vh'); - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Popup.js b/Model/Action/Dom/Popup.js index 9643eb0..3aa791a 100755 --- a/Model/Action/Dom/Popup.js +++ b/Model/Action/Dom/Popup.js @@ -81,5 +81,7 @@ export function popupButtonAction (action, callback, id) } } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Redirect.js b/Model/Action/Dom/Redirect.js index 5fc7696..5da174a 100644 --- a/Model/Action/Dom/Redirect.js +++ b/Model/Action/Dom/Redirect.js @@ -13,10 +13,16 @@ export function redirectMessage (action, callback, id) { setTimeout(function () { + const url = action.uri === '' ? '' : UriFactory.build(action.uri); + if (action.src) { - document.getElementById(action.src).src = UriFactory.build(action.uri); + document.getElementById(action.src).src = url; } else { - window.location = UriFactory.build(action.uri); + if (url === window.location.href || url === '') { + document.location.reload(); + } else { + window.location.href = url; + } } }, parseInt(action.delay)); }; diff --git a/Model/Action/Dom/Reload.js b/Model/Action/Dom/Reload.js deleted file mode 100755 index 22ef174..0000000 --- a/Model/Action/Dom/Reload.js +++ /dev/null @@ -1,30 +0,0 @@ -import { UriFactory } from '../../../Uri/UriFactory.js'; - -/** - * Reload page. - * - * @param {Object} action Action data - * @param {function} callback Callback - * @param {string} id Action element - * - * @since 1.0.0 - */ -export function reloadButtonAction (action, callback, id) -{ - 'use strict'; - - setTimeout(function () { - if (action.src) { - console.log(document.getElementById(action.src).hasAttribute('data-src')); - console.log(UriFactory.build(document.getElementById(action.src).getAttribute('data-src'))); - - document.getElementById(action.src).src = document.getElementById(action.src).hasAttribute('data-src') - ? UriFactory.build(document.getElementById(action.src).getAttribute('data-src')) - : document.getElementById(action.src).src; - } else { - document.location.reload(); - } - }, parseInt(action.delay)); - - callback(); -}; diff --git a/Model/Action/Dom/Remove.js b/Model/Action/Dom/Remove.js index f8b92aa..185eded 100755 --- a/Model/Action/Dom/Remove.js +++ b/Model/Action/Dom/Remove.js @@ -48,5 +48,7 @@ export function removeButtonAction (action, callback, id) }, 200); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/RemoveDOM.js b/Model/Action/Dom/RemoveDOM.js index 5c851ac..7e6838b 100644 --- a/Model/Action/Dom/RemoveDOM.js +++ b/Model/Action/Dom/RemoveDOM.js @@ -26,5 +26,7 @@ export function domRemoveElement (action, callback, id) e.parentElement.removeChild(e); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/RemoveValue.js b/Model/Action/Dom/RemoveValue.js index 22b8119..3532b5d 100755 --- a/Model/Action/Dom/RemoveValue.js +++ b/Model/Action/Dom/RemoveValue.js @@ -38,5 +38,7 @@ export function domRemoveValue (action, callback, id) } } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Show.js b/Model/Action/Dom/Show.js index 9b94e9f..c1faf25 100755 --- a/Model/Action/Dom/Show.js +++ b/Model/Action/Dom/Show.js @@ -21,5 +21,7 @@ export function showAction (action, callback, id) /** global: jsOMS */ jsOMS.removeClass(show, 'vh'); - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Table/Append.js b/Model/Action/Dom/Table/Append.js index f5dfe8d..0010b5e 100755 --- a/Model/Action/Dom/Table/Append.js +++ b/Model/Action/Dom/Table/Append.js @@ -43,5 +43,7 @@ export function tableAppend (action, callback) } } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Dom/Table/Clear.js b/Model/Action/Dom/Table/Clear.js index b3034ab..f25fdf7 100755 --- a/Model/Action/Dom/Table/Clear.js +++ b/Model/Action/Dom/Table/Clear.js @@ -16,5 +16,7 @@ export function tableClear (action, callback) e.removeChild(e.firstChild); } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Event/If.js b/Model/Action/Event/If.js index 1eda2c2..2754f3a 100644 --- a/Model/Action/Event/If.js +++ b/Model/Action/Event/If.js @@ -41,5 +41,7 @@ export function ifAction (action, callback, id) } } - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Event/Prevent.js b/Model/Action/Event/Prevent.js index c8ab32f..cdc7368 100755 --- a/Model/Action/Event/Prevent.js +++ b/Model/Action/Event/Prevent.js @@ -16,5 +16,7 @@ export function preventEvent (action, callback, id) jsOMS.preventAll(action.data); - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Message/Log.js b/Model/Action/Message/Log.js index fea4e4d..8950196 100755 --- a/Model/Action/Message/Log.js +++ b/Model/Action/Message/Log.js @@ -16,11 +16,13 @@ export function logAction (action, callback, id) window.omsApp.notifyManager.send( new NotificationMessage( - action.data.status, - action.data.title, - action.data.message + action.data[0].status, + action.data[0].title, + action.data[0].message ), NotificationType.APP_NOTIFICATION ); - callback(); + if (typeof callback === 'function') { + callback(); + } }; diff --git a/Model/Action/Message/Request.js b/Model/Action/Message/Request.js index 1c0f041..9028c33 100755 --- a/Model/Action/Message/Request.js +++ b/Model/Action/Message/Request.js @@ -8,10 +8,14 @@ import { Request } from '../../../Message/Request/Request.js'; * * @since 1.0.0 */ -export function requestAction (action, callback) +export function requestAction (action, callback, id) { 'use strict'; + if (action.uri === '') { + action.uri = document.getElementById(id).href; + } + /** global: jsOMS */ const request = new Request(action.uri, action.method, action.request_type); diff --git a/UI/Component/AdvancedCalendar.js b/UI/Component/AdvancedCalendar.js deleted file mode 100755 index e69de29..0000000 diff --git a/UI/Component/AdvancedRange.js b/UI/Component/AdvancedRange.js deleted file mode 100755 index e69de29..0000000 diff --git a/UI/Component/CodeArea.js b/UI/Component/CodeArea.js new file mode 100644 index 0000000..bb6c353 --- /dev/null +++ b/UI/Component/CodeArea.js @@ -0,0 +1,113 @@ +import { jsOMS } from '../../Utils/oLib.js'; + +/** + * Code text area. + * + * @copyright Dennis Eichhorn + * @license OMS License 2.0 + * @version 1.0.0 + * @since 1.0.0 + */ +export class CodeArea { + /** + * @constructor + * + * @param {string} id Form id + * + * @since 1.0.0 + */ + constructor(e) { + const self = this; + this.div = e; + this.isMouseDown = false; + + // LAYOUT (table 2 panels) + const table = document.createElement('table'); + table.setAttribute('cellspacing', '0'); + table.setAttribute('cellpadding', '0'); + + const tr = document.createElement('tr'); + const td1 = document.createElement('td'); + const td2 = document.createElement('td'); + + tr.appendChild(td1); + tr.appendChild(td2); + table.appendChild(tr); + + this.ta = this.div.querySelector('.codeTextarea'); + + // TEXTAREA NUMBERS (Canvas) + const canvas = document.createElement('canvas'); + canvas.width = 48; + canvas.height = 500; + this.ta.canvasLines = canvas; + + td1.appendChild(canvas); + td2.appendChild(this.ta); + this.div.appendChild(table); + + this.ta.addEventListener('scroll', function (e) { + self.render(); + }); + + this.ta.addEventListener('mousedown', function (e) { + self.isMouseDown = true; + }); + + this.ta.addEventListener('mouseup', function (e) { + self.isMouseDown = false; + self.render(); + }); + + this.ta.addEventListener('mousemove', function (e) { + if (self.isMouseDown) { + self.render(); + } + }); + }; + + render () { + try { + const canvas = this.ta.canvasLines; + if (canvas.height != this.ta.clientHeight) { + canvas.height = this.ta.clientHeight; // on resize + } + const ctx = canvas.getContext('2d'); + + const fontSize = parseInt(window.getComputedStyle(this.ta).fontSize); + const lineHeight = parseInt(window.getComputedStyle(this.ta).lineHeight); + + ctx.fillStyle = window.getComputedStyle(this.div).background; + ctx.fillRect(0, 0, 42, this.ta.scrollHeight + 1); + ctx.fillStyle = window.getComputedStyle(this.div).color; + ctx.font = window.getComputedStyle(this.ta).fontSize + ' monospace'; + + const startIndex = Math.floor(this.ta.scrollTop / lineHeight, 0); + const endIndex = startIndex + Math.ceil(this.ta.clientHeight / lineHeight, 0); + + let ph = 0; + let text = ''; + + for (let i = startIndex; i < endIndex; i++) { + ph = fontSize - this.ta.scrollTop + i * lineHeight; + text = '' + (1 + i); + + ctx.fillText(text, 40 - text.length * fontSize / 2, ph); + } + } catch (e) { + console.log(e.message); + } + }; +}; + +jsOMS.ready(function () +{ + 'use strict'; + + const textareas = document.querySelectorAll('.codeArea'); + const length = textareas.length; + for (let i = 0; i < length; ++i) { + const textarea = new CodeArea(textareas[i]); + textarea.render(); + } +}); diff --git a/UI/Component/Form.js b/UI/Component/Form.js index b18fb11..dfd0c4b 100755 --- a/UI/Component/Form.js +++ b/UI/Component/Form.js @@ -1284,7 +1284,7 @@ export class Form if (redirect !== null && (statusCode === 200 || statusCode === null) ) { - const redirectUrl = UriFactory.build(redirect, responseData) + const redirectUrl = UriFactory.build(redirect, responseData); fetch(redirectUrl) .then((response) => response.text()) .then((html) => { diff --git a/UI/Component/Input.js b/UI/Component/Input.js deleted file mode 100755 index cf684e2..0000000 --- a/UI/Component/Input.js +++ /dev/null @@ -1,139 +0,0 @@ -import { Request } from '../../Message/Request/Request.js'; -import { Response } from '../../Message/Response/Response.js'; -import { RequestMethod } from '../../Message/Request/RequestMethod.js'; -import { ResponseType } from '../../Message/Response/ResponseType.js'; - -/** - * Form manager class. - * - * @copyright Dennis Eichhorn - * @license OMS License 2.0 - * @version 1.0.0 - * @since 1.0.0 - */ -export class Input -{ - /** - * @constructor - * - * @since 1.0.0 - */ - constructor () - { - this.visObs = null; - }; - - /** - * Unbind input element - * - * @param {Element} input Input element - * - * @return {void} - * - * @since 1.0.0 - */ - static unbind (input) - { - this.app.inputManager.getKeyboardManager().unbind(input); - /** global: changeBind */ - // input.removeEventListener('change', changeBind, false); - }; - - /** - * Bind input element - * - * @param {Element} input Input element - * - * @return {void} - * - * @since 1.0.0 - */ - static bindElement (input = null) - { - if (input === null) { - throw new Error('Input element required'); - } - - const type = input.type; - - const removeContentButton = input.parentNode.querySelector('.close'); - if (removeContentButton !== null - && type !== 'submit' && type !== 'button') { - removeContentButton.addEventListener('click', function () { - input.value = ''; - input.focus(); - }); - } - }; - - /** - * Add remote datalist options - * - * This only applies for datalists that have remote options - * - * @param {Element} input Input element - * @param {Element} datalist Datalist element - * - * @return {void} - * - * @since 1.0.0 - */ - static addRemoteDatalistOptions (input, datalist) - { - this.clearDatalistOptions(datalist); - - const request = new Request(); - request.setData(input.value); - request.setType(ResponseType.JSON); - request.setUri(datalist.getAttribute('data-list-src')); - request.setMethod(RequestMethod.POST); - request.setRequestHeader('Content-Type', 'application/json'); - request.setSuccess(function (xhr) - { - try { - const o = JSON.parse(xhr.response); - const response = new Response(o); - const responseLength = response.count(); - let tempResponse = null; - - for (let k = 0; k < responseLength; ++k) { - tempResponse = response.getByIndex(k); - - let option = null; - const data = tempResponse.getData(); - const length = data.length; - - for (let i = 0; i < length; ++i) { - option = document.createElement('option'); - option.value = tempResponse.value; - option.text = tempResponse.text; - - datalist.appendChild(option); - } - } - } catch (exception) { - Logger.instance.error('Invalid JSON object: ' + xhr, 'FormManager'); - } - }); - - request.send(); - }; - - /** - * Remove all datalist options from datalist - * - * @param {Element} datalist Datalist element - * - * @return {void} - * - * @since 1.0.0 - */ - static clearDatalistOptions (datalist) - { - const length = datalist.options.length; - - for (let i = 0; i < length; ++i) { - datalist.remove(0); - } - }; -}; diff --git a/UI/Component/SmartTextInput.js b/UI/Component/SmartTextInput.js new file mode 100644 index 0000000..fab0b61 --- /dev/null +++ b/UI/Component/SmartTextInput.js @@ -0,0 +1,329 @@ +import { jsOMS } from '../../Utils/oLib.js'; +import { Request } from '../../Message/Request/Request.js'; + +/** + * @typedef {import('../../Event/EventManager.js').EventManager} EventManager + */ + +/** + * Smart input class. + * + * @copyright Dennis Eichhorn + * @license OMS License 2.0 + * @version 1.0.0 + * @since 1.0.0 + */ +export class SmartTextInput +{ + /** + * @constructor + * + * @param {Element} e Element to bind + * + * @since 1.0.0 + */ + constructor (e) + { + /** @type {string} id */ + this.id = e.id; + + /** @type {Element} e */ + this.inputComponent = e; + + this.inputField = this.inputComponent.getElementsByClassName('input-div')[0]; + // @todo Implement. Then find all data-tpl-value and data-tpl-text which are the elements to fill + this.dataContainer = this.inputField.getAttribute('data-container') === '' + ? this.inputComponent + : this.inputField.closest(this.inputField.getAttribute('data-container')); + this.dataList = this.inputComponent.getElementsByClassName('input-datalist')[0]; + this.dataListBody = this.inputComponent.getElementsByClassName('input-datalist-body')[0]; + this.dataTpl = document.getElementsByClassName('input-data-tpl')[0]; + this.src = this.inputComponent.getAttribute('data-src'); + + const self = this; + this.inputField.addEventListener('focus', function (e) { + self.dataList.classList.remove('vh'); + }); + + this.inputField.addEventListener('click', function (e) { + self.dataList.classList.remove('vh'); + }); + + this.inputField.addEventListener('focusout', function (e) { + setTimeout(function () { + self.dataList.classList.add('vh'); + self.clearDataListSelection(self); + + if (self.inputField.textContent === '') { + self.inputField.setAttribute('data-value', ''); + } + + if (self.inputField.classList.contains('required') && self.inputField.getAttribute('data-value') === '') { + self.inputField.classList.add('invalid'); + } else { + self.inputField.classList.remove('invalid'); + } + + const list = self.dataListBody.getElementsByTagName('div'); + const length = list.length; + + if (length > 0 && self.inputField.getAttribute('data-value') !== '') { + let isValid = false; + for (let i = 0; i < length; ++i) { + if (list[i].textContent === self.inputField.textContent) { + isValid = true; + + break; + } + } + + if (!isValid) { + self.inputField.classList.add('invalid'); + } else { + self.inputField.classList.remove('invalid'); + } + } + }, 100); + }); + + this.inputField.addEventListener('keydown', function (e) { + if (self.dataList.classList.contains("vh")) { + self.dataList.classList.remove('vh'); + } + + if (e.keyCode === 13 || e.keyCode === 40) { + jsOMS.preventAll(e); + } + + if (e.keyCode === 40) { + // down-key + self.selectOption(self.dataListBody.firstElementChild); + self.dataList.focus(); + jsOMS.preventAll(e); + } else { + // handle change delay + self.inputTimeDelay({ id: self.id, delay: 300 }, self.changeCallback, self, e); + } + }); + + // @bug This is never getting run?! + this.dataList.addEventListener('keydown', function (e) { + jsOMS.preventAll(e); + + if (e.code === 'Escape' || e.code === 'Delete' || e.code === 'Backspace') { + // handle esc, del to go back to input field + self.inputField.focus(); + self.clearDataListSelection(self); + } else if (e.code === 'ArrowUp') { + // handle up-click + if (document.activeElement.previousElementSibling !== null) { + self.clearDataListSelection(self); + self.selectOption(document.activeElement.previousElementSibling); + } + } else if (e.code === 'ArrowDown') { + // handle down-click + if (document.activeElement.nextElementSibling !== null) { + self.clearDataListSelection(self); + self.selectOption(document.activeElement.nextElementSibling); + } + } else if (e.code === 'Enter' || e.code === 'Tab') { + self.clearDataListSelection(self); + self.addToResultList(self, document.activeElement); + } + }); + + this.dataList.addEventListener('click', function (e) { + self.clearDataListSelection(self); + self.addToResultList(self, e.target); + self.dataList.classList.add('vh'); + }); + }; + + /** + * Handle remote data response result + * + * This method adds remote results to the dropdown list for selecting + * + * @param {SmartTextInput} self This reference + * @param {Object} data Response data + * + * @return {void} + * + * @since 1.0.0 + */ + remoteCallback (self, data) + { + window.omsApp.logger.log(data); + data = JSON.parse(data.response)[0]; + const dataLength = data.length; + + // if dropdown == true + if (self.dataList.getAttribute('data-active') !== 'true') { + return; + } + + if (self.inputField.textContent === '') { + self.inputField.setAttribute('data-value', ''); + } + + while (self.dataListBody.firstChild) { + self.dataListBody.removeChild(self.dataListBody.firstChild); + } + + let matchFound = false; + for (let i = 0; i < dataLength; ++i) { + // set readable value + const newRow = self.dataTpl.content.cloneNode(true); + let fields = newRow.querySelectorAll('[data-tpl-text]'); + let fieldLength = fields.length; + + for (let j = 0; j < fieldLength; ++j) { + fields[j].appendChild( + document.createTextNode( + jsOMS.getArray(fields[j].getAttribute('data-tpl-text'), data[i]) + ) + ); + } + + // set internal value + fields = newRow.querySelectorAll('[data-tpl-value]'); + fieldLength = fields.length; + + for (let j = 0; j < fieldLength; ++j) { + fields[j].setAttribute( + 'data-value', + jsOMS.getArray(fields[j].getAttribute('data-tpl-value'), data[i]) + ); + } + + // set data cache + newRow.firstElementChild.setAttribute('data-data', JSON.stringify(data[i])); + + if (!matchFound && self.inputField.textContent === newRow.firstElementChild.textContent) { + newRow.firstElementChild.classList.add('active'); + self.inputField.setAttribute('data-value', newRow.firstElementChild.getAttribute('data-value')); + + matchFound = true; + } + + self.dataListBody.appendChild(newRow); + } + + if (!matchFound && self.inputField.getAttribute('data-value') !== '') { + self.inputField.classList.add('invalid'); + } else { + self.inputField.classList.remove('invalid'); + } + }; + + /** + * Callback for input field content change + * + * @param {SmartTextInput} self This reference + * + * @return {void} + * + * @since 1.0.0 + */ + changeCallback (self) + { + // if remote data + if (typeof self.src !== 'undefined' && self.src !== '') { + const request = new Request(self.src); + request.addData(self.inputField.getAttribute('data-name'), self.inputField.textContent) + request.addData('limit', self.inputField.getAttribute('data-limit')) + + request.setSuccess(function (data) { self.remoteCallback(self, data); }); + request.send(); + } + }; + + /** + * Select element in dropdown (only mark it as selected) + * + * @param {Element} e Element to select in dropdown + * + * @return {void} + * + * @since 1.0.0 + */ + selectOption (e) + { + e.focus(); + jsOMS.addClass(e, 'active'); + }; + + /** + * Clear all selected/marked options in dropdown + * + * @param {SmartTextInput} self This reference + * + * @return {void} + * + * @since 1.0.0 + */ + clearDataListSelection (self) + { + const list = self.dataListBody.getElementsByTagName('div'); + const length = list.length; + + for (let i = 0; i < length; ++i) { + jsOMS.removeClass(list[i], 'active'); + } + }; + + /** + * Add selected dropdown elements to some final result list + * + * This can add the selected dropdown elements to a table, badge list etc. depending on the template structure. + * + * @param {SmartTextInput} self This reference + * @param {Element} e Element + * + * @return {void} + * + * @since 1.0.0 + */ + addToResultList (self, e) { + const data = JSON.parse(e.getAttribute('data-data')); + + if (self.inputField.getAttribute('data-autocomplete') === 'true') { + self.inputField.value = jsOMS.getArray(self.inputField.getAttribute('data-value'), data); + } + + self.inputField.setAttribute('data-value', e.getAttribute('data-value')); + self.inputField.textContent = e.textContent; + + self.inputField.focus(); + self.dataList.classList.add('vh'); + }; + + /** + * Delay handler (e.g. delay after finishing typing) + * + * After waiting for a delay a callback can be triggered. + * + * @param {Object} action Action type + * @param {function} callback Callback to be triggered + * @param {SmartTextInput} self This reference (passed to callback) + * @param {Object} data Data (passed to callback) + * + * @return {void} + * + * @since 1.0.0 + */ + inputTimeDelay (action, callback, self, data) + { + if (SmartTextInput.timerDelay[action.id]) { + clearTimeout(SmartTextInput.timerDelay[action.id]); + delete SmartTextInput.timerDelay[action.id]; + } + + SmartTextInput.timerDelay[action.id] = setTimeout(function () { + delete SmartTextInput.timerDelay[action.id]; + callback(self, data); + }, action.delay); + }; +}; + +SmartTextInput.timerDelay = {}; diff --git a/UI/Component/AdvancedInput.js b/UI/Component/TagInput.js old mode 100755 new mode 100644 similarity index 95% rename from UI/Component/AdvancedInput.js rename to UI/Component/TagInput.js index b255ce3..0946526 --- a/UI/Component/AdvancedInput.js +++ b/UI/Component/TagInput.js @@ -13,7 +13,7 @@ import { Request } from '../../Message/Request/Request.js'; * @version 1.0.0 * @since 1.0.0 */ -export class AdvancedInput +export class TagInput { /** * @constructor @@ -149,7 +149,7 @@ export class AdvancedInput * * This method adds remote results to the dropdown list for selecting * - * @param {AdvancedInput} self This reference + * @param {TagInput} self This reference * @param {Object} data Response data * * @return {void} @@ -217,7 +217,7 @@ export class AdvancedInput /** * Callback for input field content change * - * @param {AdvancedInput} self This reference + * @param {TagInput} self This reference * * @return {void} * @@ -257,7 +257,7 @@ export class AdvancedInput /** * Clear all selected/marked options in dropdown * - * @param {AdvancedInput} self This reference + * @param {TagInput} self This reference * * @return {void} * @@ -283,7 +283,7 @@ export class AdvancedInput * * This can add the selected dropdown elements to a table, badge list etc. depending on the template structure. * - * @param {AdvancedInput} self This reference + * @param {TagInput} self This reference * @param {Element} e Element * * @return {void} @@ -379,7 +379,7 @@ export class AdvancedInput * * @param {Object} action Action type * @param {function} callback Callback to be triggered - * @param {AdvancedInput} self This reference (passed to callback) + * @param {TagInput} self This reference (passed to callback) * @param {Object} data Data (passed to callback) * * @return {void} @@ -388,16 +388,16 @@ export class AdvancedInput */ inputTimeDelay (action, callback, self, data) { - if (AdvancedInput.timerDelay[action.id]) { - clearTimeout(AdvancedInput.timerDelay[action.id]); - delete AdvancedInput.timerDelay[action.id]; + if (TagInput.timerDelay[action.id]) { + clearTimeout(TagInput.timerDelay[action.id]); + delete TagInput.timerDelay[action.id]; } - AdvancedInput.timerDelay[action.id] = setTimeout(function () { - delete AdvancedInput.timerDelay[action.id]; + TagInput.timerDelay[action.id] = setTimeout(function () { + delete TagInput.timerDelay[action.id]; callback(self, data); }, action.delay); }; }; -AdvancedInput.timerDelay = {}; +TagInput.timerDelay = {}; diff --git a/UI/GeneralUI.js b/UI/GeneralUI.js index a82c9c7..0d9ec3e 100755 --- a/UI/GeneralUI.js +++ b/UI/GeneralUI.js @@ -1,7 +1,8 @@ import { jsOMS } from '../Utils/oLib.js'; import { UriFactory } from '../Uri/UriFactory.js'; -import { AdvancedInput } from './Component/AdvancedInput.js'; +import { TagInput } from './Component/TagInput.js'; +import { SmartTextInput } from './Component/SmartTextInput.js'; // import { NotificationLevel } from '../Message/Notification/NotificationLevel.js'; // import { NotificationMessage } from '../Message/Notification/NotificationMessage.js'; // import { NotificationType } from '../Message/Notification/NotificationType.js'; @@ -196,7 +197,7 @@ export class GeneralUI } e[i].addEventListener('load', function () { - const spinner = this.parentElement.getElementsByClassName('ispinner'); + const spinner = this.parentElement.getElementsByClassName('spinner'); if (spinner.length > 0) { spinner[0].style.display = 'none'; @@ -256,14 +257,28 @@ export class GeneralUI */ bindInput (e = null) { - e = e !== null + let l = e !== null ? [e] : document.getElementsByClassName('advIpt'); - const length = e.length; + let length = l.length; for (let i = 0; i < length; ++i) { - new AdvancedInput(e[i], this.app.eventManager, this.app.uiManager.getDOMObserver()); // eslint-disable-line no-new + new TagInput(l[i], this.app.eventManager, this.app.uiManager.getDOMObserver()); // eslint-disable-line no-new + } + + l = e !== null + ? [e] + : document.querySelectorAll('.smart-input-wrapper'); + + length = l.length; + + for (let i = 0; i < length; ++i) { + if (!l[i].querySelector('.input-div').hasAttribute('contenteditable')) { + continue; + } + + new SmartTextInput(l[i]); // eslint-disable-line no-new } }; diff --git a/UI/Input/Keyboard/KeyboardManager.js b/UI/Input/Keyboard/KeyboardManager.js index 2bfafab..930b738 100755 --- a/UI/Input/Keyboard/KeyboardManager.js +++ b/UI/Input/Keyboard/KeyboardManager.js @@ -60,6 +60,11 @@ export class KeyboardManager for (let i = 0; i < length; ++i) { elements[i].addEventListener('keydown', function (event) { + if (self.down.includes(event.keyCode)) { + // Already fired + return; + } + self.down.push(event.keyCode); self.run(element, event); }); @@ -90,7 +95,7 @@ export class KeyboardManager run (element, event) { if (typeof this.elements[element] === 'undefined') { - throw new Error('Unexpected elmenet!'); + throw new Error('Unexpected element!'); } const actions = this.elements[element].concat(this.elements['']); diff --git a/Uri/UriFactory.js b/Uri/UriFactory.js index 808bd58..8651420 100755 --- a/Uri/UriFactory.js +++ b/Uri/UriFactory.js @@ -212,14 +212,16 @@ export class UriFactory } } - let parsed = uri.replace(new RegExp('\{[\/#\?%@\.\$\!].*?\}', 'g'), function (match) { + let parsed = uri.replace(new RegExp('\{[\/#\?%@°\.\$\!].*?\}', 'g'), function (match) { match = match.substring(1, match.length - 1); if (toMatch !== null && (Object.prototype.hasOwnProperty.call(toMatch, match) || match.includes('/')) ) { - return match.includes('/') ? jsOMS.getArray(match, toMatch) : toMatch[match]; + return typeof toMatch[match] === 'undefined' + ? (match.includes('/') ? jsOMS.getArray(match, toMatch) : toMatch[match]) + : toMatch[match] } else if (typeof UriFactory.uri[match] !== 'undefined') { return UriFactory.uri[match]; } else if (match.indexOf('!') === 0) { @@ -268,6 +270,20 @@ export class UriFactory return current.query(); } else if (match.indexOf('/') === 0 && match.length === 1) { return current.path; + } else if (match.indexOf('°') === 0 && match.length === 1) { + if(!navigator.geolocation) { + return; + } + + var lat = 0.0; + var lon = 0.0; + + navigator.geolocation.getCurrentPosition(position => { + lat = position.coords.latitude; + lon = position.coords.longitude; + }); + + return lat === 0.0 && lon === 0.0 ? '' : lat + ',' + lon; } else if (match.indexOf(':user') === 0) { return current.user; } else if (match.indexOf(':pass') === 0) { diff --git a/Utils/Debug.js b/Utils/Debug.js deleted file mode 100755 index 7692c0d..0000000 --- a/Utils/Debug.js +++ /dev/null @@ -1,113 +0,0 @@ -const visited = []; -const findings = {}; -const cssSelectors = {}; -const cssFiles = ['http://127.0.0.1/cssOMS/styles.css']; -const cssFilesLength = cssFiles.length; -const domain = window.location.hostname; -const pageLimit = 10; - -const cssRequest = new XMLHttpRequest(); -cssRequest.onreadystatechange = function () -{ - if (cssRequest.readyState === 4 && cssRequest.status === 200) { - const cssText = this.responseText; - const result = cssText.match(/[a-zA-Z0-9\ :>~\.\"'#,\[\]=\-\(\)\*]+{/g); - const resultLength = result.length; - - for (let i = 1; i < resultLength; ++i) { - const sel = result[i].substring(0, result[i].length - 1).trimLeft().trimRight().trimRight(); - if (!Object.prototype.hasOwnProperty.call(cssSelectors, sel)) { - cssSelectors[sel] = 0; - } - } - } -}; - -for (let i = 0; i < cssFilesLength; ++i) { - cssRequest.open('GET', cssFiles[i], true); - cssRequest.send(); -} - -const validatePage = function (url) -{ - if (visited.includes(url) || visited.length > pageLimit - 1) { - return; - } - - // mark url as visited - visited.push(url); - findings[url] = {}; - - // web request - const webRequest = new XMLHttpRequest(); - webRequest.onreadystatechange = function () - { - if (webRequest.readyState === 4 && webRequest.status === 200) { - // replace content - document.open(); - document.write(this.responseText); - document.close(); - - // analyze img alt attribute - const imgAlt = document.querySelectorAll('img:not([alt]), img[alt=""], img[alt=" "]'); - findings[url].img_alt = imgAlt.length; - - // analyze img src - const imgSrc = document.querySelectorAll('img:not([src]), img[src=""], img[src=" "]'); - findings[url].img_src = imgSrc.length; - - // analyze empty link - const aHref = document.querySelectorAll('a:not([alt]), a[href=""], a[href=" "], a[href="#"]'); - findings[url].href_empty = aHref.length; - - /* eslint-disable max-len */ - // analyze inline on* function - const onFunction = document.querySelectorAll('[onafterprint], [onbeforeprint], [onbeforeunload], [onerror], [onhaschange], [onload], [onmessage], [onoffline], [ononline], [onpagehide], [onpageshow], [onpopstate], [onredo], [onresize], [onstorage], [onundo], [onunload], [onblur], [onchage], [oncontextmenu], [onfocus], [onformchange], [onforminput], [oninput], [oninvalid], [onreset], [onselect], [onsubmit], [onkeydown], [onkeypress], [onkeyup], [onclick], [ondblclick], [ondrag], [ondragend], [ondragenter], [ondragleave], [ondragover], [ondragstart], [ondrop], [onmousedown], [onmousemove], [onmouseout], [onmouseover], [onmouseup], [onmousewheel], [onscroll], [onabort], [oncanplay], [oncanplaythrough], [ondurationchange], [onemptied], [onended], [onerror], [onloadeddata], [onloadedmetadata], [onloadstart], [onpause], [onplay], [onplaying], [onprogress], [onratechange], [onreadystatechange], [onseeked], [onseeking], [onstalled], [onsuspend], [ontimeupdate], [onvolumechange], [onwaiting]'); - findings[url].js_on = onFunction.length; - - // analyze missing form element attributes - const formElements = document.querySelectorAll('input:not([id]), input[type=""], select:not([id]), textarea:not([id]), label:not([for]), label[for=""], label[for=" "], input:not([name]), select:not([name]), textarea:not([name]), form:not([id]), form:not([action]), form[action=""], form[action=" "], form[action="#"]'); - findings[url].form_elements = formElements.length; - - // analyze invalid container-children relationship (e.g. empty containers, invalid children) - const invalidContainerChildren = document.querySelectorAll(':not(tr) > td, :not(tr) > th, colgroup *:not(col), :not(colgroup) > col, tr > :not(td):not(th), optgroup > :not(option), :not(select) > option, :not(fieldset) > legend, select > :not(option):not(optgroup), :not(select):not(optgroup) > option, table > *:not(thead):not(tfoot):not(tbody):not(tr):not(colgroup):not(caption)'); - findings[url].invalid_container_children = invalidContainerChildren.length; - /* eslint-enable max-len */ - - // has inline styles - const hasInlineStyles = document.querySelectorAll('*[style]'); - findings[url].form_elements = hasInlineStyles.length; - - // analyze css usage - let cssFound; - for (const i in cssSelectors) { - try { - cssFound = document.querySelectorAll(i.replace(/:hover|:active/gi, '')); - cssSelectors[i] = cssFound === null ? 0 : cssFound.length; - } catch (e) {} - } - - // check other pages - const links = document.querySelectorAll('a'); - const linkLength = links.length; - - for (let i = 0; i < linkLength; ++i) { - if (visited.includes(links[i].href) - || (!links[i].href.startsWith('/') && links[i].href.startsWith('http') && !links[i].href.includes(domain)) - ) { - continue; - } - - validatePage(links[i].href); - } - } - }; - - webRequest.open('GET', url, true); - webRequest.send(); -}; - -validatePage(location.href); - -console.table(findings); -console.table(cssSelectors); diff --git a/Views/FormView.js b/Views/FormView.js index 21428ad..d26ecf3 100755 --- a/Views/FormView.js +++ b/Views/FormView.js @@ -1,5 +1,4 @@ import { jsOMS } from '../Utils/oLib.js'; -import { Input } from '../UI/Component/Input.js'; /** * Form view. @@ -13,14 +12,14 @@ import { Input } from '../UI/Component/Input.js'; * @version 1.0.0 * @since 1.0.0 * - * @tood Karaka/jsOMS#60 - * On change listener - * Allow to add a on change listener in a form. This should result in automatic submits after changing a form. - * Consider the following cases to submit the form: - * * on Enter (all except textarea) - * * on Change (by using a timer) - * * on Leave (all elements) - * The listener should be defined in the form definition once and in js be applied to all form elements. + * @todo On change listener + * Allow to add a on change listener in a form. This should result in automatic submits after changing a form. + * Consider the following cases to submit the form: + * * on Enter (all except textarea) + * * on Change (by using a timer) + * * on Leave (all elements) + * The listener should be defined in the form definition once and in js be applied to all form elements. + * https://github.com/Karaka-Management/jsOMS/issues/60 */ export class FormView { @@ -776,27 +775,6 @@ export class FormView } else { this.action = 'EMPTY'; } - - const elements = this.getFormElements(); - const length = elements.length; - - for (let i = 0; i < length; ++i) { - switch (elements[i].tagName.toLowerCase()) { - case 'input': - Input.bindElement(elements[i]); - break; - case 'select': - // this.bindSelect(elements[i]); - break; - case 'textarea': - // this.bindTextarea(elements[i]); - break; - case 'button': - // this.bindButton(elements[i]); - break; - default: - } - } }; getElementsToBind (e = null)