diff --git a/Message/Request/Request.js b/Message/Request/Request.js index 010d2e9..98c8997 100644 --- a/Message/Request/Request.js +++ b/Message/Request/Request.js @@ -36,7 +36,7 @@ this.result[0] = function() { - jsOMS.Log.Logger.instance.error('Invalid response code.'); + jsOMS.Log.Logger.instance.info('Unhandled response'); }; /** global: XMLHttpRequest */ diff --git a/Model/Action/Utils/Timer.js b/Model/Action/Utils/Timer.js index 7d36229..c847633 100644 --- a/Model/Action/Utils/Timer.js +++ b/Model/Action/Utils/Timer.js @@ -7,7 +7,7 @@ * @since 1.0.0 */ const timerActionDelay = {}; -const timerAction = function (action, callback) +const timerAction = function (action, callback, data) { "use strict"; @@ -18,6 +18,6 @@ const timerAction = function (action, callback) timerActionDelay[action.id] = setTimeout(function() { delete timerActionDelay[action.id]; - callback(); + callback(data); }, action.delay); }; diff --git a/UI/Component/AdvancedInput.js b/UI/Component/AdvancedInput.js new file mode 100644 index 0000000..89363be --- /dev/null +++ b/UI/Component/AdvancedInput.js @@ -0,0 +1,235 @@ +/** + * Advanced input class. + * + * @copyright Dennis Eichhorn + * @license OMS License 1.0 + * @version 1.0.0 + * @since 1.0.0 + * + * @todo: this class is probably the most stupid thing I've done in a long time. Seriously fix this! + * @todo: Passing self to every MEMBER function is just dumb. + */ +(function (jsOMS) +{ + "use strict"; + + /** @namespace jsOMS.UI */ + jsOMS.Autoloader.defineNamespace('jsOMS.UI.Component'); + + jsOMS.UI.Component.AdvancedInput = class { + /** + * @constructor + * + * @since 1.0.0 + */ + constructor (e) + { + this.id = e.id; + this.inputComponent = e; + this.inputField = this.inputComponent.getElementsByClassName('input')[0]; + this.dropdownElement = document.getElementById(this.id + '-dropdown'); + this.tagElement = document.getElementById(this.id + '-tags'); + this.dataList = this.dropdownElement.getElementsByTagName('table')[0]; + this.dataListBody = this.dataList.getElementsByTagName('tbody')[0]; + this.dataTpl = document.getElementById(this.id + '-rowElement'); + this.tagTpl = this.tagElement.getElementsByTagName('template')[0]; + this.src = this.inputField.getAttribute('data-src'); + + const self = this; + this.inputField.addEventListener('keydown', function(e) { + if (e.keyCode === 13 || e.keyCode === 40) { + jsOMS.preventAll(e); + } + + if (e.keyCode === 40) { + self.selectOption(self.dataListBody.firstElementChild); + } else { + // handle change delay + self.inputTimeDelay({id: self.id, delay: 300}, self.changeCallback, self, e); + } + }); + + this.dropdownElement.addEventListener('keydown', function(e) { + jsOMS.preventAll(e); + + // todo: consider if it makes sense to have a none element always for phone users only to jump out? + // todo: if not remote then the suggestion dropdown should filter itself based on best match + // todo: dropdown should show/hide or depending on setting be always visible maybe with :focus+table or similar + + if (e.keyCode === 27 || e.keyCode === 46 || e.keyCode === 8) { + // handle esc, del to go back to input field + self.inputField.focus(); + self.clearDataListSelection(self); + } else if (e.keyCode === 38) { + // handle up-click + if (document.activeElement.previousElementSibling !== null) { + self.clearDataListSelection(self); + self.selectOption(document.activeElement.previousElementSibling); + } + } else if (e.keyCode === 40) { + // handle down-click + if (document.activeElement.nextElementSibling !== null) { + self.clearDataListSelection(self); + self.selectOption(document.activeElement.nextElementSibling); + } + } else if (e.keyCode === 13 || e.keyCode === 9) { + self.clearDataListSelection(self); + self.addToResultList(self); + } + }); + + this.dropdownElement.addEventListener('focusout', function(e){ + self.clearDataListSelection(self); + }); + + this.dropdownElement.addEventListener('click', function(e) { + if (document.activeElement.tagName.toLowerCase() !== 'tr') { + return; + } + + self.clearDataListSelection(self); + self.addToResultList(self); + }); + }; + + remoteCallback(self, data) + { + data = JSON.parse(data.response); + const dataLength = data.length; + + console.table(data); + + // if dropdown == true + if (self.dropdownElement.getAttribute('data-active') === 'true') { + while (self.dataListBody.firstChild) { + self.dataListBody.removeChild(self.dataListBody.firstChild); + } + + 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]) + ); + } + + self.dataListBody.appendChild(newRow); + } + } + }; + + changeCallback(self, key) + { + // if remote data + if (typeof self.src !== 'undefined' && self.src !== '') { + const request = new jsOMS.Message.Request.Request(self.src); + request.setSuccess(function (data) { self.remoteCallback(self, data); }); + request.send(); + } + }; + + selectOption(e) + { + e.focus(); + // todo: change to set style .active + e.setAttribute('style', 'background: #f00'); + jsOMS.addClass(e, 'active'); + }; + + clearDataListSelection(self) + { + const list = self.dataListBody.getElementsByTagName('tr'), + length = list.length; + + for (let i = 0; i < length; ++i) { + // todo: remove the active class + list[i].setAttribute('style', ''); + jsOMS.removeClass(list[i], 'active'); + } + }; + + addToResultList(self) { + if (self.inputField.getAttribute('data-autocomplete') === 'true') { + self.inputField.value = document.activeElement.querySelectorAll('[data-tpl-value="' + self.inputField.getAttribute('data-value') + '"]')[0].getAttribute('data-value'); + } + + if (self.tagElement.getAttribute('data-active') === 'true') { + // todo: make badges removable + const newTag = self.tagTpl.content.cloneNode(true); + + // set internal value + let fields = newTag.querySelectorAll('[data-tpl-value]'); + let fieldLength = fields.length; + let uuid = ''; + let value = ''; + + for (let j = 0; j < fieldLength; ++j) { + value = document.activeElement.querySelectorAll('[data-tpl-value="' + newTag.firstElementChild.getAttribute('data-tpl-value') + '"]')[0].getAttribute('data-value'); + fields[j].setAttribute('data-value', value); + + uuid += value; + } + + // don't allow duplicate + if (self.tagElement.querySelectorAll('[data-tpl-uuid="' + uuid + '"').length !== 0) { + return; + } + + newTag.firstElementChild.setAttribute('data-tpl-uuid', uuid); + + // set readable text + fields = newTag.querySelectorAll('[data-tpl-text]'); + fieldLength = fields.length; + + for (let j = 0; j < fieldLength; ++j) { + fields[j].appendChild( + document.createTextNode( + document.activeElement.querySelectorAll('[data-tpl-text="' + fields[j].getAttribute('data-tpl-text') + '"]')[0].innerText + ) + ); + } + + // allow limit + if (self.tagElement.childElementCount >= self.tagElement.getAttribute('data-limit') + && self.tagElement.getAttribute('data-limit') != 0 + ) { + self.tagElement.removeChild(self.tagElement.firstElementChild); + } + + self.tagElement.appendChild(newTag); + } + }; + + inputTimeDelay(action, callback, self, data) + { + if (jsOMS.UI.Component.AdvancedInput.timerDelay[action.id]) { + clearTimeout(jsOMS.UI.Component.AdvancedInput.timerDelay[action.id]); + delete jsOMS.UI.Component.AdvancedInput.timerDelay[action.id] + } + + jsOMS.UI.Component.AdvancedInput.timerDelay[action.id] = setTimeout(function() { + delete jsOMS.UI.Component.AdvancedInput.timerDelay[action.id]; + callback(self, data); + }, action.delay); + }; + } + + jsOMS.UI.Component.AdvancedInput.timerDelay = {}; +}(window.jsOMS = window.jsOMS || {})); \ No newline at end of file diff --git a/UI/Component/Autocomplete.js b/UI/Component/Autocomplete.js deleted file mode 100644 index 2ac9d49..0000000 --- a/UI/Component/Autocomplete.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Autocomplete class. - * - * @copyright Dennis Eichhorn - * @license OMS License 1.0 - * @version 1.0.0 - * @since 1.0.0 - */ -(function (jsOMS) -{ - "use strict"; - - /** @namespace jsOMS.UI */ - jsOMS.Autoloader.defineNamespace('jsOMS.UI.Component'); -}(window.jsOMS = window.jsOMS || {})); \ No newline at end of file diff --git a/UI/Component/Input.js b/UI/Component/Input.js index dd37468..d7d5f52 100644 --- a/UI/Component/Input.js +++ b/UI/Component/Input.js @@ -14,6 +14,16 @@ jsOMS.Autoloader.defineNamespace('jsOMS.UI.Input'); jsOMS.UI.Input = class { + /** + * @constructor + * + * @since 1.0.0 + */ + constructor () + { + this.visObs = null; + }; + /** * Unbind input element * @@ -49,6 +59,7 @@ input.addEventListener('change', function changeBind(event) { + console.log('ttttttt'); /* Handle remote datalist/autocomplete input element */ let listId, list; if (typeof (listId = this.getAttribute('list')) !== 'undefined' && (list = document.getElementById(listId)).getAttribute('data-list-src') !== 'undefined') { diff --git a/UI/Component/Tab.js b/UI/Component/Tab.js index 4542f09..c7dc459 100644 --- a/UI/Component/Tab.js +++ b/UI/Component/Tab.js @@ -18,8 +18,9 @@ * * @since 1.0.0 */ - constructor () + constructor (app) { + this.app = app; }; /** @@ -41,7 +42,7 @@ } } else { const tabs = document.querySelectorAll('.tabview'), - length = !tabs ? 0 : tabs.length; + length = !tabs ? 0 : tabs.length; for (let i = 0; i < length; ++i) { this.bindElement(tabs[i]); @@ -52,7 +53,7 @@ /** * Bind & rebind UI element. * - * @param {Object} e Element id + * @param {Object} e Element * * @return {void} * @@ -60,23 +61,55 @@ */ bindElement (e) { - const nodes = e.querySelectorAll('.tab-links a'); + this.activateTabUri(e); - nodes.addEventListener('click', function (evt) - { - /* Change Tab */ - const attr = this.getAttribute('href').substring(1), - cont = this.parentNode.parentNode.parentNode.children[1]; + const nodes = e.querySelectorAll('.tab-links li'), + length = nodes.length; - jsOMS.removeClass(jsOMS.getByClass(this.parentNode.parentNode, 'active'), 'active'); - jsOMS.addClass(this.parentNode, 'active'); - jsOMS.removeClass(jsOMS.getByClass(cont, 'active'), 'active'); - jsOMS.addClass(jsOMS.getByClass(cont, attr), 'active'); + for (let i = 0; i < length; ++i) { + nodes[i].addEventListener('click', function (evt) + { + let fragmentString = window.location.href.includes('#') ? jsOMS.Uri.Http.parseUrl(window.location.href).fragment : ''; - /* Modify url */ + /* Change Tab */ + /* Remove selected tab */ + fragmentString = jsOMS.ltrim(fragmentString.replace(this.parentNode.getElementsByClassName('active')[0].getElementsByTagName('label')[0].getAttribute('for'), ''), ','); + jsOMS.removeClass(this.parentNode.getElementsByClassName('active')[0], 'active'); + jsOMS.addClass(this, 'active'); - jsOMS.preventAll(evt); - }); + /* Add selected tab */ + window.history.pushState(null,'', + jsOMS.Uri.UriFactory.build( + '{%}#' + (fragmentString === '' ? '' : fragmentString + ',') + this.getElementsByTagName('label')[0].getAttribute('for') + ) + ); + }); + } + }; + + /** + * Activates the correct tab based on URI fragment. + * + * This allows to link a specific open tab to a user or make it a bookmark + * + * @param {Object} e Element + * + * @return {void} + * + * @since 1.0.0 + */ + activateTabUri(e) + { + const fragmentString = window.location.href.includes('#') ? jsOMS.Uri.Http.parseUrl(window.location.href).fragment : ''; + const fragments = fragmentString.split(','), + fragLength = fragments.length; + + for (let i = 0; i < fragLength; ++i) { + let label = e.querySelectorAll('label[for="' + fragments[i] + '"]')[0]; + if (typeof label !== 'undefined') { + label.click(); + } + } }; } }(window.jsOMS = window.jsOMS || {})); diff --git a/UI/Component/TagInput.js b/UI/Component/TagInput.js deleted file mode 100644 index 2e01ff8..0000000 --- a/UI/Component/TagInput.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Form manager class. - * - * @copyright Dennis Eichhorn - * @license OMS License 1.0 - * @version 1.0.0 - * @since 1.0.0 - */ -(function (jsOMS) -{ - "use strict"; - - /** @namespace jsOMS.UI */ - jsOMS.Autoloader.defineNamespace('jsOMS.UI.Component'); -}(window.jsOMS = window.jsOMS || {})); \ No newline at end of file diff --git a/UI/Component/TagList.js b/UI/Component/TagList.js deleted file mode 100644 index 2e01ff8..0000000 --- a/UI/Component/TagList.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Form manager class. - * - * @copyright Dennis Eichhorn - * @license OMS License 1.0 - * @version 1.0.0 - * @since 1.0.0 - */ -(function (jsOMS) -{ - "use strict"; - - /** @namespace jsOMS.UI */ - jsOMS.Autoloader.defineNamespace('jsOMS.UI.Component'); -}(window.jsOMS = window.jsOMS || {})); \ No newline at end of file diff --git a/UI/GeneralUI.js b/UI/GeneralUI.js index 8ffbf5d..68f3b2c 100644 --- a/UI/GeneralUI.js +++ b/UI/GeneralUI.js @@ -36,12 +36,13 @@ bind (id) { let e = null; - if (typeof id !== 'undefined') { + if (typeof id !== 'undefined' && id !== null) { e = document.getElementById(id); } this.bindHref(e); this.bindLazyLoad(e); + this.bindInput(e); }; /** @@ -102,5 +103,24 @@ } } }; + + /** + * Bind & rebind UI element. + * + * @param {Object} [e] Element id + * + * @return {void} + * + * @since 1.0.0 + */ + bindInput (e) + { + e = e !== null ? [e] : document.getElementsByClassName('advancedInput'); + const length = e.length; + + for (let i = 0; i < length; ++i) { + new jsOMS.UI.Component.AdvancedInput(e[i]); + } + }; } }(window.jsOMS = window.jsOMS || {})); diff --git a/UI/UIManager.js b/UI/UIManager.js index 9efe42b..496d0e4 100644 --- a/UI/UIManager.js +++ b/UI/UIManager.js @@ -25,7 +25,7 @@ { this.app = app; this.formManager = new jsOMS.UI.Component.Form(this.app); - this.tabManager = new jsOMS.UI.Component.Tab(); + this.tabManager = new jsOMS.UI.Component.Tab(this.app); this.tableManager = new jsOMS.UI.Component.Table(this.app); this.actionManager = new jsOMS.UI.ActionManager(this.app); this.dragNDrop = new jsOMS.UI.DragNDrop(this.app); diff --git a/Uri/UriFactory.js b/Uri/UriFactory.js index c35eeba..9d50aea 100644 --- a/Uri/UriFactory.js +++ b/Uri/UriFactory.js @@ -123,6 +123,7 @@ */ static unique (url) { + // unique queries const parts = url.replace(/\?/g, '&').split('&'), full = parts[0]; @@ -148,6 +149,28 @@ url = full + '?' + pars.join('&'); } + // unique fragments + const fragments = url.match(/\#[a-zA-Z0-9\-,]+/g), + fragLength = fragments !== null ? fragments.length : 0; + + for (let i = 0; i < fragLength; ++i) { + url = url.replace(fragments[i], ''); + } + + if (fragLength > 0) { + const fragList = fragments[fragLength - 1].split(','), + fragListLength = fragList.length; + let fragListNew = []; + + for (let i = 0; i < fragListLength; ++i) { + if (!fragListNew.includes(fragList[i]) && fragList[i] !== '') { + fragListNew.push(fragList[i]); + } + } + + url += fragListNew.join(','); + } + return url; };