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.
+ * - css selector for a containing element, optional
+ * - css selector for elements the DatasetSupport works with primarily
+ *
+ */
+ 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
* - keymapper - function that returns a key of a user data. Used to identify user data in the Dataset. Mandatory.
@@ -386,17 +388,20 @@ class Dataset {
*
* @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