diff --git a/src/main/webapp/resources/css/styles.css b/src/main/webapp/resources/css/styles.css index 76f8cad..bcc479e 100644 --- a/src/main/webapp/resources/css/styles.css +++ b/src/main/webapp/resources/css/styles.css @@ -120,6 +120,23 @@ ul.nav-tabs > li.nav-item { display: inline-block; } +.sortable, .sort-asc, .sort-desc { + cursor: pointer; +} +.sortable::after, .sort-asc::after, .sort-desc::after { + color: #b2babb; + float: inline-end; +} +.sortable::after { + content: '\002B27'; /* #11047, 2B27 */ +} +.sort-asc::after { + content: '\002BC5'; /* #11205, 2BC5 */ +} +.sort-desc::after { + content: '\002BC6'; /* #11206, 2BC6 */ +} + .avatar { width: auto; } diff --git a/src/main/webapp/resources/js/base/base.js b/src/main/webapp/resources/js/base/base.js index c3ce975..25dcc1b 100644 --- a/src/main/webapp/resources/js/base/base.js +++ b/src/main/webapp/resources/js/base/base.js @@ -632,9 +632,9 @@ $.fn.setPaging = function(config) { return this.each(function(){ - let length = config.list ? config.list.length : config.dataLength, + let length = config.list ? config.list.length : config.dataSize || config.dataLength, empty = length < 1, - start = empty ? 0 : config.start + 1, + start = empty ? 0 : !config.added ? config.start + 1 : 1, end = empty ? 0 : config.start + length, pagingInfo = empty ? "" : start + " ~ " + numberFormat.format(end) + " / " + numberFormat.format(config.totalSize), selector = "#"+ config.prefix + "PagingInfo,[name='" + config.prefix + "PagingInfo']"; @@ -673,7 +673,7 @@ $.fn.setCurrentRow = function(val) { var e = $(this); e.find("tr").each(function(){ var tr = $(this), - current = val == tr.attr("data-key"); + current = val == (tr.attr("data-key") || tr.attr("data-index")); if (current) tr.addClass("current-row"); else @@ -799,4 +799,84 @@ function inputsInRange(fromSource, toSource) { function ignore() { console.log.apply(console, arguments); +} + +/**Utility to simplify the document object query. + */ +class DomQuery { + /**Sets the selectors to the containers. + * @param {string} containers selectors(comma-separated) to the containers + * @returns {DomQuery} this DomQuery + */ + setContainers(containers = "") { + this.containers = containers.split(",").filter(str => !isEmpty(str)); + return this; + } + + /**Returns a selector prepended with the selectors to the containers. + * @param {...string} selectors + * To get a selector prepended with the selectors to the containers + *
let doq = new DomQuery().setContainers("div#divID,form#formID"),
+	 *     selector = doq.selector("input[type='text']");
+ * To get a selector with the given attribute value prepended with the selectors to the containers. + *
selector = doq.selector("attribute-name", "attribute-value");
+ * @returns {string} selector prepended with the selectors to the containers + */ + selector(...selectors) { + let result = null; + + if (!this.containers) + this.containers = []; + + let selector = ""; + switch (selectors.length) { + case 1: selector = selectors[0]; break; + case 2: selector = "[" + selectors[0] + "='" + selectors[1] + "']"; break; + default: break; + } + + if (this.containers.length < 1) + result = selector; + + if (!result && !selector.includes(",")) + result = this.containers + .map(container => selector ? container + " " + selector : container) + .join(","); + + if (!result && selector.includes(",")) { + selector = selector.split(","); + result = this.containers.reduce((acc, x) => [...acc, ...selector.map(y => x + " " + y)], []).join(","); + } + + if (this.trace) + log("selector:", result); + + return result; + } + + /**Selects an element matching the given selector. + * @param {...string} args + * To get an element matching the given selector + *
let doq = new DomQuery().setContainers("div#divID,form#formID"),
+	 *     found = doq.find("selector to the desired element");
+ * To get an element with the given attribute value. + *
found = doq.find("attribute-name", "attribute-value");
+ * @returns {object} element matching the given selector + */ + find(...args) { + return document.querySelector(this.selector(...args)); + } + + /**Selects elements matching the given selector. + * @param {...string} args + * To get elements matching the given selector + *
let doq = new DomQuery().setContainers("div#divID,form#formID"),
+	 *     found = doq.findAll("selector to the desired elements");
+ * To get elements with the given attribute value. + *
found = doq.findAll("attribute-name", "attribute-value");
+ * @returns {array} elements matching the given selector + */ + findAll(...args) { + return Array.from(document.querySelectorAll(this.selector(...args))); + } } \ No newline at end of file diff --git a/src/main/webapp/resources/js/base/dataset-support.js b/src/main/webapp/resources/js/base/dataset-support.js new file mode 100644 index 0000000..7c4dfb1 --- /dev/null +++ b/src/main/webapp/resources/js/base/dataset-support.js @@ -0,0 +1,387 @@ +/* Copyright (c) 2020 Emjay Khan. All rights reserved. */ + +/**Base class of DatasetSupport. + * It has empty implementations of methods called back in response to a Dataset's events. + * A DatasetSupport extension implements some of the call-back methods to achieve its purpose. + */ +class DatasetSupport { + /**Creates a DatasetSupport with the given configuration. + * @param {object} conf configuration. + * + */ + constructor(conf) { + this.selector = conf.selector || null; + this.dataset = conf.ctrl.dataset; + this._doq = conf.ctrl.doq; + } + + get doq() { + return this._doq; + } + + init() {} + + get keymapped() { + return this.dataset ? this.dataset.keymapped : false; + } + + getSelector(...selectors) { + return this.doq.selector(...selectors); + } + + /** + * @param {string} selector + * @param {boolean} strict + */ + find(...selector) { + return this.doq.find(...selector); + } + + findAll(...selector) { + return this.doq.findAll(...selector); + } +} + +class TableSupport extends DatasetSupport { + constructor(conf) { + super(conf); + + this.selector = conf.table; + this.tr = conf.tr; + this.notFound = conf.notFound; + + this.formatter = conf.formatter; + this.selectionToggler = conf.selectionToggler || ""; + this.refreshOnModify = conf.refreshOnModify || []; + + this.init(); + } + + init() { + this.body = this.find(this.selector + " tbody"); + + if (this.tr) { + let template = this.find(this.tr); + this.tr = (template || {}).innerHTML || ""; + } + if (this.notFound) { + let template = this.find(this.notFound); + this.notFound = (template || {}).innerHTML || ""; + } + + if (!this.tr && !this.notFound) { + let templates = this.findAll(this.selector + " template"); + if (templates.length < 1) + log("WARNING: ", this.selector + " must have a template for a data row"); + this.tr = (templates[0] || {}).innerHTML; + this.notFound = (templates[1] || {}).innerHTML; + } + + let sort = evt => { + let th = evt.target; + this.dataset.sort(th.getAttribute("data-sort")); + }; + this.sortables().forEach(th => th.addEventListener("click", sort)); + } + + sortables() { + return this.findAll(this.selector + " thead th[data-sort]"); + } + + updateSortables(sorter) { + this.sortables().forEach(th => { + th.classList.remove( + TableSupport.cssClass.sortable, + TableSupport.cssClass.asc, + TableSupport.cssClass.desc + ); + if (th.getAttribute("data-sort") == sorter.by) + th.classList.add(TableSupport.cssClass[sorter.order]); + else + th.classList.add(TableSupport.cssClass.sortable); + }); + if (sorter.by) + this.draw(); + } + + /**Handler called back on the dataset change event. + * @param {Dataset} dataset this Dataset + * @param {object} option optional information + */ + renderList(option = {}) { + this.draw(option); + + this.findAll(".enable-onfound").forEach(e => e.disabled = this.dataset.empty); + } + + draw(option = {}) { + let empty = this.dataset.empty, + trs = !empty ? this.dataset.inStrings(this.tr, this.formatter) : [this.notFound]; + this.body.innerHTML = trs.join(""); + + if (!this.selectionToggler) return; + + let toggler = this.find(this.selectionToggler); + toggler.checked = false; + toggler.disabled = empty; + } + + /**Handler called back on the current change event. + * @param {DataItem} item current DataItem + */ + setCurrentRow(item) { + if (!item) return; + + let index = item.index; + this.findAll(this.selector + " tbody tr").forEach(tr => { + let dataIndex = tr.getAttribute("data-index"), + current = index == dataIndex; + if (current) + tr.classList.add(TableSupport.cssClass.current); + else + tr.classList.remove(TableSupport.cssClass.current); + }); + } + + /**Handler called back on the selection change event + * @param {array} selected selected DataItems + */ + setSelections(selected) { + let selectedIndex = selected.map(item => item.index); + this.findAll(this.selector + " tbody input[name='data-index']") + .forEach(input => { + input.checked = selectedIndex.includes(input.value) + }); + + this.findAll(".enable-onselect") + .forEach(e => e.disabled = selected.length < 1); + } + + /**Handler called back on the modify event + * @param {array} changed names of the changed properties + * @param {DataItem} item owner DataItem of the changed properties + * @param {boolean} current whether the event is on the current item + */ + updateModified(changed) { + if (this.refreshOnModify.length < 1) return; + + let refresh = false; + for (let prop of changed) { + refresh = this.refreshOnModify.includes(prop); + if (refresh) + break; + } + if (!refresh) return; + + this.draw(); + this.dataset.setState(); + } +} +TableSupport.cssClass = { + current: "current-row", + sortable: "sortable", + asc: "sort-asc", + desc: "sort-desc" +}; + +class CurrentDataSupport extends DatasetSupport { + constructor(conf) { + super(conf); + if (!this.selector) + this.selector = "[data-field]"; + } + + type(input) { + return input ? (input.getAttribute("type") || input.tagName).toLowerCase() : ""; + } + + dataField(input) { + return input.getAttribute("data-field"); + } + + property(input) { + return this.dataField(input) + || input.name + || input.id; + } + + update(input, value) { + switch (this.type(input)) { + case "radio": + case "checkbox": + if (isEmpty(value)) { + input.checked = false; + } else { + switch (typeof value) { + case "string": + input.checked = input.value == value || value.split(",").includes(input.value); + break; + case "number": + case "boolean": + input.checked = input.value == value.toString(); + break; + default: + if (value instanceof Array) + input.checked = value.includes(input.value); + break; + } + } + break; + case "select": + for (let option of input.options) { + option.selected = option.value === value; + } + break; + case "img": input.src = value; break; + case "button": input.innerHTML = value; break; + default: + if (input.value !== undefined) + input.value = value; + else + input.innerHTML = value; + } + } + + setChanged(evt) { + let input = evt.target, + type = this.type(input), + prop = this.property(input), + val = input.value; + + switch (type) { + case "checkbox": + case "radio": + let attrs = input.getAttributeNames() + .filter(attr => ['data-field', 'name'].includes(attr)) + .map(attr => "[" + attr + "=\"" + input.getAttribute(attr) + "\"]") + .join(""); + let inputs = this.findAll("[type=\"" + type + "\"]" + attrs); + switch (inputs.length) { + case 0: return; + case 1: + let cb = inputs[0]; + if (cb.checked) break; + + switch (val) { + case "true": val = "false"; break; + case "y": val = "n"; break; + case "Y": val = "N"; break; + case "yes": val = "no"; break; + default: val = ""; break; + } + break; + default: + val = inputs.filter(cb => cb.checked).map(cb => cb.value).join(","); + break; + } + break; + default: break; + } + + this.dataset.setValue(prop, val); + } + + setCurrent(item) { + if (!item) return; + + this.findAll(this.selector).forEach(input => { + let prop = this.property(input); + if (!prop) return; + + let evt = !["checkbox", "radio"].includes(this.type(input)) ? "change" : "click", + handler = (evt) => this.setChanged(evt) + + input.removeEventListener(evt, handler); + this.update(input, item.getValue(prop)); + input.addEventListener(evt, handler); + }); + this.enableOnDirty(item); + this.enableOnNew(item); + } + + updateModified(changed, item) { + if (!item) return; + + this.findAll(this.selector).forEach(input => { + let prop = this.property(input); + if (!changed.includes(prop)) return; + + this.update(input, item.getValue(prop)); + }); + this.enableOnDirty(item); + } + + /**Enables the HTML elements of '.enable-ondirtyitem' if the item is dirty. + * @param {DataItem} item a DataItem + */ + enableOnDirty(item) { + let dirty = item.dirty; + + this.findAll(".enable-ondirtyitem") + .forEach(e => e.disabled = !dirty); + } + + /**Enables the HTML elements of '.enable-onnewitem' if the item is new. + * @param {DataItem} item a DataItem + */ + enableOnNew(item) { + let isnew = item.isNew(); + + this.findAll(".enable-onnewitem") + .forEach(e => e.disabled = !isnew); + } + + newData(init) { + let data = this.findAll(this.selector).reduce((data, input) => { + let prop = this.property(input); + if (prop) + data[prop] = null; + return data; + }, {}); + if (init) + init(data); + return data; + } + + getData(item = this.dataset.getCurrent("item")) { + return this.findAll(this.selector).reduce((data, input) => { + let dataField = this.dataField(input), + property = input.getAttribute("name") || input.getAttribute("id"), + val = item.data[dataField]; + if (property && val !== undefined) + data[property] = val; + return data; + }, {}); + } +} + +class PagingSupport extends DatasetSupport { + constructor(conf) { + super(conf); + + this.prefix = conf.ctrl.prefix; + this.sizeOffset = conf.sizeOffset || 0; + + this.linkContainer = conf.linkContainer; + this.func = conf.func; + + this.statusContainer = conf.statusContainer; + this.statusContent = conf.statusContent; + } + + setPaging(option) { + let pagination = option ? option.pagination : null; + if (!pagination) return; + + pagination.prefix = this.prefix; + pagination.dataSize = pagination.dataSize + this.sizeOffset; + + if (this.linkContainer) { + pagination.func = this.func; + pagination.added = option.added; + $(this.doq.selector(this.linkContainer)).setPaging(pagination); + } + } +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/base/dataset.js b/src/main/webapp/resources/js/base/dataset.js index 58a266d..9c80784 100644 --- a/src/main/webapp/resources/js/base/dataset.js +++ b/src/main/webapp/resources/js/base/dataset.js @@ -187,20 +187,13 @@ ValueFormat.InvalidValue = "^invalid^value^"; /**Wraps a user data and traces the manipulation performed on it and consequent status. */ class DataItem { - /** user data */ - data; - /** value formatters */ - _formats; - /** whether the user data is selected or not */ - selected; - /** state of the user data */ - state; - /**Creates a new DataItem. * @param {any} data user data * @param {object} formats value formatters of the user data's property */ constructor(data, formats) { + this.index = null; + this.no = null; this.data = data; this._formats = formats; this.selected = false; @@ -291,6 +284,16 @@ class DataItem { this.data[property] = parsed; return parsed; } + + /**Replaces the current data with the new data and resets the state. + * @param {any} data new data + * @returns the DataItem + */ + replace(data) { + this.data = data; + this.state = null; + return this; + } /**Returns a string converted from the template using the property values of the user data. * In the template, placeholder for the properties of the user data is specified like {property name}. @@ -303,10 +306,18 @@ class DataItem { if (formatter) { str = formatter(str, this); } - for (let p in this.data) { - let regexp = this._formats.regexp(p); - str = str.replace(regexp, this.getValue(p)); - } + let empty = Object.entries(this.data).length < 1 + if (!empty) + for (let p in this.data) { + let regexp = this._formats.regexp(p); + str = str.replace(regexp, this.getValue(p)); + } + str = str.replace(/{data-index}/gi, this.index) + .replace(/{data-no}/gi, this.no); + + if (empty) + str = str.replace(/{([^}]+)}/g, ""); + return str; } } @@ -357,15 +368,6 @@ class DataItem { *

*/ class Dataset { - _items; - _byKeys; - _current; - - /**Dataset configuration - */ - conf; - _formats; - /**Creates a new Dataset with a configuration. * The configuration is an object with which you specify * * @param conf {object} configuration */ - constructor(conf) { + constructor(conf = {}) { this._items = []; + this.keys = conf.keys || []; this._byKeys = {}; this._current = null; this.conf = notEmpty(conf, "conf is required but missing"); - notEmpty(conf.keymapper, "keymapper is required but missing"); let keymapper = conf.keymapper; - conf.keymapper = info => { - return keymapper(info) || info._tmpKey; - }; + if (keymapper) + conf.keymapper = info => { + return keymapper(info) || info._tmpKey; + }; + this.keymapped = !isEmpty(this.conf.keymapper); + this._formats = new ValueFormat(conf.formats); this._sorter = {by: ""}; this.dataBinder = dataBinder.create(this, conf.doctx); @@ -404,6 +409,7 @@ class Dataset { if (!conf.trace) this.log = () => {}; + [ "onDatasetChange", "onCurrentChange", "onSelectionChange", @@ -428,12 +434,20 @@ class Dataset { } /**Returns the key of a user data. - * @param {any|DataItem} info user data or {@link DataItem dataItem} of a user data + * @param {any|DataItem} item user data or {@link DataItem dataItem} of a user data * @returns {string} key of a user data */ - getKey(info) { - let data = info ? info.data || info : null; - return data ? this.conf.keymapper(data) : null; + getKey(item) { + if (!item) + return null; + + let dataItem = item instanceof DataItem, + info = dataItem ? item.data : item; + if (this.keymapped) + return this.conf.keymapper(info); + else if (dataItem) + return item.index; + throw "Unable to determine the key of " + info; } /**Returns keys of the Dataset's user data. @@ -457,7 +471,7 @@ class Dataset { * let removed = dirties.removed; */ getKeys(status){ - let dataset = this.getDataset(status); + let dataset = this.getDataset(status, "item"); if ("dirty" != status) return dataset.map(e => this.getKey(e)); @@ -479,9 +493,20 @@ class Dataset { * let dataItem = dataset.getData("key-0", "item"); */ getData(key, option) { - let item = this._byKeys["key-" + key]; - if (!item) - item = this.getTempItem(); + if (this.empty) + return null; + + let item = null; + + if (this.keymapped) { + item = this._byKeys["key-" + key]; + if (!item) + item = this.getTempItem(); + } else { + let index = key, + found = this._items.filter(item => index == item.index); + item = found.length > 0 ? found[0] : null; + } if (!item || item.unreachable) return null; return "item" == option ? item : item.data; @@ -500,28 +525,17 @@ class Dataset { * @param {object} obj optional information * @returns {Dataset} the Dataset */ - setData(obj, option) { + setData(obj, option = {}) { this._byKeys = {}; this._current = null; obj = obj || {}; - let data = this._getDataItems(obj); + let data = this._getDataItems(obj, option); this._items = data.items; this._byKeys = data.byKeys; this._sorter = {by: ""}; - /* - obj = obj || {}; - let array = Array.isArray(obj) ? obj : this.conf.dataGetter(obj) || []; - if (!Array.isArray(array)) - throw new Error("The data must be an array"); - - this._items = array.map(e => new DataItem(e, this._formats)); - this._items.forEach(item => { - let key = "key-" + this.getKey(item.data); - this._byKeys[key] = item; - }); - */ + this.onDatasetChange(obj, option); this.setState(obj.state); this.onDirtiesChange(this.dirty); @@ -529,17 +543,34 @@ class Dataset { return this; } - _getDataItems(obj) { + _getDataItems(obj, option) { obj = obj || {}; let array = Array.isArray(obj) ? obj : this.conf.dataGetter(obj) || []; if (!Array.isArray(array)) throw new Error("The data must be an array"); - let _items = array.map(e => new DataItem(e, this._formats)), - _byKeys = {}; - _items.forEach(item => { - let key = "key-" + this.getKey(item.data); - _byKeys[key] = item; + let prefix = "ndx-" + new Date().getTime(), + _items = array.map(e => new DataItem(e, this._formats)), + _byKeys = {}, + length = this._items.length, + noStart = 0; + + if (option.pagination) + noStart = option.pagination.start || 0; + else { + if (length > 0) { + let last = this._items[length - 1]; + noStart = last.no; + } + } + + _items.forEach((item, index) => { + if (this.keymapped) { + let key = "key-" + this.getKey(item.data); + _byKeys[key] = item; + } + item.index = prefix + index; + item.no = ++noStart; }); return { @@ -561,12 +592,13 @@ class Dataset { * @param {object} obj optional information * @returns {Dataset} the Dataset */ - addData(obj, option) { + addData(obj, option = {}) { if (this.empty) - return this.setData(obj); + return this.setData(obj, option); let state = this.state; - let data = this._getDataItems(obj); + option.added = true; + let data = this._getDataItems(obj, option); this._items = this._items.concat(data.items); this._byKeys = { ...this._byKeys, @@ -614,10 +646,10 @@ class Dataset { this._items.sort((item0, item1) => { let val0 = (item0.data || {})[by], val1 = (item1.data || {})[by]; - if (val0 === undefined || val1 === undefined) - throw "Property not found: " + by; + if (val0 === undefined || val1 === undefined) return 0; if (isEmpty(val0) && isEmpty(val1)) return 0; + if (!this._sorter.asc) [val0, val1] = [val1, val0]; @@ -645,7 +677,7 @@ class Dataset { return { by: this._sorter.by, - order: order + order: order }; } @@ -689,10 +721,18 @@ class Dataset { } getTempItem(current) { - let found = this._items.filter(item => item.data._tmpKey); + let found = this._items.filter(item => item.data._tmpKey || item.isNew()); found = found.length > 0 ? found[0] : null; - if (current) - this.setCurrent(); + if (this.keymapped) { + if (current) + this.setCurrent(); + } else { + if (found && current) { + this._current = found; + this.dataBinder.onCurrentChange(found); + this.onCurrentChange(found); + } + } return found; } @@ -726,8 +766,9 @@ class Dataset { get state() { let empty = this.empty, self = this; + return { - currentKey:!empty ? self.getKey(self.getCurrent()) : null, + currentKey:!empty ? self.getKey(self.getCurrent("item")) : null, selectedKeys:!empty ? self.getKeys("selected") : [] }; } @@ -753,9 +794,10 @@ class Dataset { this.onDirtiesChange(false); } else { state = state || this.state; - let current = this.getData(state.currentKey) || this.getDataset()[0], + let current = this.getData(state.currentKey, "item") || this.getDataset("item")[0], currentKey = this.getKey(current); - this.onSort(this.sorter); + + this.onSort(this.sorter); this.setCurrent(currentKey, true); this.select(state.selectedKeys || [], true, true); } @@ -896,7 +938,8 @@ class Dataset { fire = args[2]; } if (dirty || fire) { - this.onSelectionChange(this.getDataset("selected")); + let selected = this.getDataset("selected", this.keymapped ? undefined : "item"); + this.onSelectionChange(selected); } return dirty; } @@ -914,8 +957,10 @@ class Dataset { */ toggle(key) { let item = this.getData(key, "item"), - status = item ? item.toggle() : false; - this.onSelectionChange(this.getDataset("selected")); + status = item ? item.toggle() : false, + selected = this.getDataset("selected", this.keymapped ? undefined : "item"); + + this.onSelectionChange(selected) return status; } @@ -934,26 +979,41 @@ class Dataset { if (!data) return this; let found = this.getTempItem(true); - if (found) { + if (found) return found; - } let notDirty = !this.dirty, - array = Array.isArray(data) ? data : [data]; - array.forEach(e => { + array = Array.isArray(data) ? data : [data], + now = new Date().getTime(), + length = this._items.length, + noStart = 0, + added = []; + + if (length > 0) { + let last = this._items[length - 1]; + noStart = last.no; + } + array.forEach((e, index) => { let item = new DataItem(e, this._formats); this._items.push(item); - let key = this.getKey(e); - if (!key) - e._tmpKey = key = new Date().getTime() - this._byKeys["key-" + key] = item; + if (this.keymapped) { + let key = this.getKey(e); + if (!key) + e._tmpKey = key = now + index; + this._byKeys["key-" + key] = item; + } else { + item.index = ("ndx-" + now) + index; + item.no = ++noStart; + } item.state = "added"; + added.push(item); }); let state = this.state; this.onAppend(array); - state.currentKey = this.getKey(array[array.length - 1]); + let last = added[added.length - 1]; + state.currentKey = this.keymapped ? this.getKey(last.data) : last.index; this.setState(state); if (notDirty) @@ -1057,31 +1117,44 @@ class Dataset { if (isEmpty(replacement)) return this; let replacements = Array.isArray(replacement) ? replacement : [replacement], - replacing = []; + replacing = [], + getKey = obj => { + return this.keymapped ? + (obj.key || this.getKey(obj.data)) : + this.keys.reduce((acc, cur) => { + acc[cur] = obj.data[cur]; + return acc; + }, {} + ); + }, + getItem = key => { + if (this.keymapped) + return this.getData(key, "item"); + + let entries = Object.entries(key), + found = this._items.filter(item => { + for (let e of entries) { + let k = e[0], + v = e[1]; + if (v != item.data[k]) + return false; + } + return true; + }); + return found.length < 1 ? null : found[0]; + }; replacements.forEach(obj => { let data = obj.data; if (!data) return; - let key = obj.key || this.getKey(data); + let key = getKey(obj); if (!key) return; - let oldItem = this.getData(key, "item"), - newItem = new DataItem(data, this._formats), - pos = oldItem ? this._items.indexOf(oldItem) : -1; - - newItem.selected = oldItem && oldItem.selected; - if (pos > -1) - this._items[pos] = newItem; - else - this._items.push(newItem); - - delete this._byKeys["key-" + key]; - this._byKeys["key-" + this.getKey(data)] = newItem; + let item = getItem(key); + if (!item) return; - if (this._current == oldItem) - this._current = newItem; - - replacing.push(newItem); + item.replace(data); + replacing.push(item); }); this.onReplace(replacing); this.setState(); @@ -1108,7 +1181,7 @@ class Dataset { let before = this.dirty, keys = Array.isArray(key) ? key : [key], removed = this._items.filter(item => { - let k = this.getKey(item.data), + let k = this.getKey(this.keymapped ? item.data : item), remove = keys.includes(k); if (remove) { item.state = "added" == item.state ? "ignore" : "removed"; @@ -1167,7 +1240,7 @@ class Dataset { let before = this.dirty, keys = Array.isArray(key) ? key : [key], erased = this._items.filter(item => { - let k = this.getKey(item.data), + let k = this.getKey(this.keymapped ? item.data : item), erase = keys.indexOf(k) > -1; if (erase) { delete this._byKeys["key-" + k]; @@ -1216,7 +1289,7 @@ class Dataset { let dataset = this.getDataset("item"); return dataset.filter(item => !item.data._tmpKey) - .map(item => item.inString(template, formatter)); + .map((item, index) => item.inString(template, formatter)); } /**Returns a property value of user data. @@ -1231,20 +1304,24 @@ class Dataset { */ getValue(...args) { let key = null, - property = null; + property = null, + item = null; + switch (args.length) { case 1: - key = this.getKey(this.getCurrent()); + //key = this.getKey(this.getCurrent()); property = args[0]; + item = this.getCurrent("item"); break; case 2: key = args[0]; property = args[1]; + item = this.getData(key, "item"); break; default: return null; } - let item = this.getData(key, "item"); +// let item = this.getData(key, "item"); return item ? item.getValue(property) : undefined; } @@ -1268,7 +1345,7 @@ class Dataset { value = null; switch (args.length) { case 2: - key = this.getKey(this.getCurrent()); + key = this.getKey(this.getCurrent(this.keymapped ? undefined : "item")); property = args[0]; value = args[1]; break; @@ -1279,6 +1356,7 @@ class Dataset { break; default: return this; } + return this.modify(key, function(item){ return item.setValue(property, value); }); @@ -1338,10 +1416,15 @@ class Dataset { class DatasetControl { constructor(conf) { - notEmpty(conf.keymapper, "keymapper"); this.prefix = conf.prefix; this.prefixName = conf.prefixName; - this.doctx = conf.doctx || ""; +// this.doctx = conf.doctx || ""; + this.doq = new DomQuery().setContainers(conf.doctx); + + if (conf.addOns) { + conf.addOns.forEach(addOn => addOn.doq = this.doq); + } + this.infoSize = conf.infoSize; this.appendData = conf.appendData; @@ -1360,6 +1443,7 @@ class DatasetControl { */ this.onModify(props, modified, current); }; + conf.onDirtiesChange = dirty => this.onDirtiesChange(dirty); conf.onReplace = obj => this.onReplace(obj); conf.onSort = status => this.onSort(status); @@ -1374,6 +1458,10 @@ class DatasetControl { }; } + get addOns() { + return this.dataset.addOns; + } + prefixed(str) { return (this.prefix || "") + str; } @@ -1387,8 +1475,7 @@ class DatasetControl { this._load(); } - _load(option) { - option = option || {}; + _load(option = {}) { if (!this.query.pageNum) this.query.pageNum = 1; @@ -1443,11 +1530,22 @@ class DatasetControl { }); } - setData(obj, option) { + setData(obj, option = {}) { + this.setPaging(obj, option); this.dataset.setData(obj, option); } - addData(obj, option) { + setPaging(obj, option) { + let prefix = this.prefix || obj.prefix; + if (!prefix || !obj[prefix + "Paging"]) return; + + option.pagination = obj[prefix + "Paging"]; + option.pagination.prefix = prefix; + delete obj[prefix + "Paging"]; + } + + addData(obj, option = {}) { + this.setPaging(obj, option); this.dataset.addData(obj, option); } @@ -1463,8 +1561,11 @@ class DatasetControl { return this.dataset.getCurrent(option); } - toObject(item) { - return this.dataBinder().toObject(item || this.getCurrent("item")); + toObject(item = this.getCurrent("item")) { + if (this.dataset.keymapped) + return this.dataBinder().toObject(item); + if (this.addOns.currentData) + return this.addOns.currentData.getData(item); } dataBinder() { @@ -1515,7 +1616,8 @@ class DatasetControl { setInfo(info) {} newInfo(obj) { - this.dataset.append(obj || {}); + if (this.dataset.keymapped) + this.dataset.append(obj || {}); this.getInfo(); } @@ -1535,6 +1637,10 @@ class DatasetControl { debug("on modify", props, "modified", modified, "current", current); } + onDirtiesChange(dirty) { + debug("on dirties change", dirty); + } + onReplace(replacing) { debug("on replace", replacing); } @@ -1585,23 +1691,15 @@ class DatasetControl { } selector(selector) { - return this.dataBinder().selector(selector); - } - - querySelector(selector) { - return this.dataBinder().querySelector(selector); + return this.doq.selector(selector); } - querySelectorAll(selector) { - return this.dataBinder().querySelectorAll(selector); + find(...args) { + return this.doq.find(...args); } - find(name) { - return this.querySelector(!name.startsWith("#") ? "[name='" + name + "']" : name); - } - - findAll(name) { - return this.querySelectorAll("[name='" + name + "']"); + findAll(...args) { + return this.doq.findAll(...args); } } @@ -1644,7 +1742,7 @@ var dataBinder = { }, create: (dataset, doctx) => { - if (!dataset || isEmpty(doctx)) + if (!dataset || isEmpty(doctx) || !dataset.keymapped) return { onCurrentChange: item => {}, onModify: (changed, item) => {}, @@ -1708,6 +1806,7 @@ var dataBinder = { }); }; obj.onModify = (changed, item) => { + return; obj.inputs().forEach(input => { let prop = dataBinder.dataMap(input) || dataBinder.property(input); if (!changed.includes(prop)) return; @@ -1739,26 +1838,26 @@ var dataBinder = { */ function tableSorter(ctrl, selector) { let obj = { - asc: "⯅", - desc: "⯆", - + asc: "sort-asc", + desc: "sort-desc", + sortable: "sortable", + ctrl: ctrl, - headers: () => ctrl.querySelectorAll(selector), + headers: () => ctrl.findAll(selector), sort: (evt) => { let th = evt.target; - ctrl.sort(th.getAttribute("data-field")); + ctrl.sort(th.getAttribute("data-sort")); } }; obj.setHeaders = (sorter) => { - obj.headers().forEach(th => { - let inner = th.innerHTML; - - inner = inner.replace(/ ⯅/gi, "").replace(/ ⯆/gi, ""); - if (th.getAttribute("data-field") == sorter.by) - inner += " " + obj[sorter.order] || ""; - th.innerHTML = inner; + obj.headers().forEach(th => { + th.classList.remove(obj.sortable, obj.asc, obj.desc); + if (th.getAttribute("data-sort") == sorter.by) + th.classList.add(obj[sorter.order]); + else + th.classList.add(obj.sortable); }); }; ctrl.onSort = (sorter) => { @@ -1768,23 +1867,8 @@ function tableSorter(ctrl, selector) { }; obj.headers().forEach(th => { - th.style["cursor"] = "pointer"; th.removeEventListener("click", obj.sort); th.addEventListener("click", obj.sort); }); return obj; -/* - asc: {style: "sorting_asc", attr: "ascending"}, - desc: {style: "sorting_desc", attr: "descending"}, - setHeaders: (headers, sorter) => { - headers.forEach(th => { - th.classList.remove("sorting_asc", "sorting_desc"); - th.removeAttribute("aria-sort"); - if (th.getAttribute("data-field") != sorter.by) return; - - th.classList.add(sorting[sorter.order].style); - th.setAttribute("aria-sort", sorting[sorter.order].attr); - }); - } -*/ } \ No newline at end of file