You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1749 lines
48 KiB
JavaScript
1749 lines
48 KiB
JavaScript
/* Copyright (c) 2020 Emjay Khan. All rights reserved. */
|
|
|
|
/**@file Classes and objects to help control user data in HTML pages
|
|
*/
|
|
|
|
function lpad(v) {
|
|
return v < 10 ? "0" + v : v;
|
|
}
|
|
|
|
/** value format for numbers */
|
|
const numberFormat = {
|
|
/**Parses the value for a number
|
|
* @param {(string|number)} value value to parse
|
|
* @returns {number} number parsed from the value
|
|
*/
|
|
parse(value) {
|
|
if (!value) return 0;
|
|
|
|
switch (typeof(value)) {
|
|
case "number": return value;
|
|
case "string":
|
|
let num = Number(value.replace(/[\s,]/g, ""));
|
|
if (!isNaN(num))
|
|
return num;
|
|
default: return ValueFormat.InvalidValue;
|
|
}
|
|
},
|
|
|
|
/**Formats the value
|
|
* @param {number} value value to format
|
|
* @returns {string} formatted value
|
|
*/
|
|
format(value) {
|
|
let num = numberFormat.parse(value);
|
|
return num != ValueFormat.InvalidValue ? Number(num).toLocaleString() : ValueFormat.InvalidValue;
|
|
}
|
|
};
|
|
|
|
/** value format for dates */
|
|
const dateFormat = {
|
|
/**Formats the value
|
|
* @param {number} value value to format
|
|
* @returns {string} formatted value
|
|
*/
|
|
format(value) {
|
|
if (isEmpty(value)) return "";
|
|
|
|
let _format = v => {
|
|
let date = "number" == typeof v ? new Date(v) : v;
|
|
let year = date.getFullYear(),
|
|
month = lpad(date.getMonth() + 1),
|
|
day = lpad(date.getDate());
|
|
return year + "-" + month + "-" + day;
|
|
};
|
|
|
|
switch (value instanceof Date ? "date" : typeof value) {
|
|
case "number":
|
|
case "date": return _format(value);
|
|
case "string": return value.substr(0, 4) + "-" + value.substr(4, 2) + "-" + value.substr(6, 2);
|
|
default: return "";
|
|
}
|
|
},
|
|
|
|
parse(value) {
|
|
return isEmpty(value) ? "" : value.replace(/-/gi, "");
|
|
}
|
|
};
|
|
|
|
/** value format for time */
|
|
const timeFormat = {
|
|
/**Formats the value
|
|
* @param {number} value value to format
|
|
* @returns {string} formatted value
|
|
*/
|
|
format(value) {
|
|
let _format = v => {
|
|
let date = "number" == typeof v ? new Date(v) : v,
|
|
hours = lpad(date.getHours()),
|
|
minutes = lpad(date.getMinutes()),
|
|
seconds = lpad(date.getSeconds());
|
|
return hours + ":" + minutes + ":" + seconds;
|
|
};
|
|
|
|
switch (value instanceof Date ? "date" : typeof value) {
|
|
case "number":
|
|
case "date": return _format(value);
|
|
case "string":
|
|
let offset = value.length - 6;
|
|
return value.substr(0 + offset, 2) + ":" + value.substr(2 + offset, 2) + ":" + value.substr(4 + offset)
|
|
default: return "";
|
|
}
|
|
},
|
|
|
|
parse(value) {
|
|
return isEmpty(value) ? "" : value.replace(/:/gi, "");
|
|
}
|
|
}
|
|
|
|
/** value format for datetimes */
|
|
const datetimeFormat = {
|
|
/**Formats the value
|
|
* @param {number} value value to format
|
|
* @returns {string} formatted value
|
|
*/
|
|
format(value) {
|
|
switch (value instanceof Date ? "date" : typeof value) {
|
|
case "number":
|
|
case "date": return dateFormat.format(value) + " " + timeFormat.format(value);
|
|
case "string": return dateFormat.format(value) + " " + timeFormat.format(value);
|
|
default: return "";
|
|
}
|
|
},
|
|
|
|
parse(value) {
|
|
return isEmpty(value) ? "" : timeFormat.parse(dateFormat.parse(value)).replace(/ /gi, "");
|
|
}
|
|
};
|
|
|
|
/**Manages value formats.
|
|
* A value format is an object that has functions
|
|
* <ul> <li><code>parse(arg)</code> that parses the argument for a value</li>
|
|
* <li><code>format(arg)</code> that formats the argument to a string</li>
|
|
* </ul>
|
|
* And each value format is associatd with a key as follows.
|
|
* <pre><code>let valueFormats = new ValueFormat({
|
|
* key0: numberFormat,
|
|
* key1: dateFormat,
|
|
* key2: {
|
|
* format(value) {...},
|
|
* parse(value) {...}
|
|
* }
|
|
* })</code></pre>
|
|
*/
|
|
class ValueFormat {
|
|
_formats;
|
|
_exprs;
|
|
|
|
/**Creates a new ValueFormat.
|
|
* @param {object} formats an object whose property names are keys and property values are value formats associated with the keys
|
|
*/
|
|
constructor(formats) {
|
|
this._formats = formats || {};
|
|
this._exprs = {};
|
|
}
|
|
|
|
/**Returns a parser associated with the key.
|
|
* @param {string} key key associated with a value format
|
|
* @returns {function}
|
|
* <ul> <li>parser associated with the key</li>
|
|
* <li>if not found for the key, default parser</li>
|
|
* </ul>
|
|
*/
|
|
parser(key) {
|
|
let parser = this._formats[key];
|
|
return parser && parser.parse ? parser.parse : ValueFormat.Default.parse;
|
|
}
|
|
|
|
/**Returns a formatter associated with the key.
|
|
* @param {string} key key associated with a value format
|
|
* @returns {function}
|
|
* <ul> <li>formatter associated with the key</li>
|
|
* <li>if not found for the key, default formatter</li>
|
|
* </ul>
|
|
*/
|
|
formatter(key) {
|
|
let formatter = this._formats[key];
|
|
return formatter && formatter.format ? formatter.format : ValueFormat.Default.format;
|
|
}
|
|
|
|
regexp(key) {
|
|
let expr = this._exprs[key];
|
|
if (!expr)
|
|
this._exprs[key] = expr = new RegExp("{" + key + "}", "g");
|
|
return expr;
|
|
}
|
|
}
|
|
|
|
/**Default ValueFormat
|
|
*/
|
|
ValueFormat.Default = {
|
|
parse(value) {return value;},
|
|
format(value) {return ifEmpty(value, "");}
|
|
};
|
|
/** Represents an invalid value. */
|
|
ValueFormat.InvalidValue = "^invalid^value^";
|
|
|
|
/**Wraps a user data and traces the manipulation performed on it and consequent status.
|
|
*/
|
|
class DataItem {
|
|
/**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;
|
|
this.state = null;
|
|
}
|
|
|
|
/**selects or unselects the user data.
|
|
* @param {boolean} select
|
|
* <ul><li>false to unselect the user data</li>
|
|
* <li>true or undefined to select the user data</li>
|
|
* </ul>
|
|
* @returns {boolean} whether selection status is changed
|
|
* <ul><li>true if selection status is changed</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
* @example
|
|
* <ul><li>to select, dataItem.select() or dataItem.select(true)</li>
|
|
* <li>to unselect, dataItem.select(false)</li>
|
|
* </ul>
|
|
*/
|
|
select(select) {
|
|
let arg = ifEmpty(select, true),
|
|
dirty = this.selected != arg;
|
|
this.selected = arg;
|
|
return dirty;
|
|
}
|
|
|
|
/**Returns whether the user data is either created, modified, or removed.
|
|
* @returns {boolean}
|
|
* <ul><li>true if the user data is either created, modified, or removed</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
get dirty() {
|
|
return ["added", "modified", "removed"].includes(this.state);
|
|
}
|
|
|
|
/**Returns whether the user data is unreachable.
|
|
* @returns {boolean}
|
|
* <ul><li>true if the user data is unreachable</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
get unreachable() {
|
|
return ["removed", "ignore"].includes(this.state);
|
|
}
|
|
|
|
/**Returns whether the user data is created and not saved yet.
|
|
* @returns {boolean}
|
|
* <ul><li>true if the user data is new</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
isNew() {
|
|
return "added" == this.state;
|
|
}
|
|
|
|
/**Toggles the selection status.
|
|
* @returns {boolean} current selection status
|
|
* <ul><li>true if the user data is selected</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
toggle() {
|
|
return this.selected = !this.selected;
|
|
}
|
|
|
|
/**Returns the formatted value of the named property of the user data.
|
|
* @param {string} property property name
|
|
* @returns {any} formatted value of the named property of the user data
|
|
*/
|
|
getValue(property) {
|
|
let value = this.data[property];
|
|
return this._formats.formatter(property)(value);
|
|
}
|
|
|
|
/**Parses and sets the value to the named property of the user data.
|
|
* @param {string} property property name
|
|
* @param {any} value value
|
|
* @return {any}
|
|
* <ul><li>parsed value</li>
|
|
* <li>ValueFormat.InvalidValue if failed to parse the value</li>
|
|
* </ul>
|
|
*/
|
|
setValue(property, value) {
|
|
let parsed = this._formats.parser(property)(value);
|
|
if (ValueFormat.InvalidValue != parsed)
|
|
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}.
|
|
* @param {string} template template string
|
|
* @param {function} formatter function to format a row string with custom property placeholders
|
|
* @returns {string} string converted from the template using the property values of the user data
|
|
*/
|
|
inString(template, formatter) {
|
|
let str = template;
|
|
if (formatter) {
|
|
str = formatter(str, this);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**Manages user data wrapped in {@link DataItem}s, tracing the state after manipulation performed on them.
|
|
* <p>For a Dataset to work properly, it needs a keymapper to identify user data.
|
|
* And you specify it in a Dataset's configuration.
|
|
* <pre><code>let dataset = new Dataset({
|
|
* keymapper: function(info) {return info.keyProperty;},
|
|
* ...
|
|
* });</code></pre>
|
|
* </p>
|
|
* <p>To help access values of user data, a Dataset offers methods
|
|
* <ul><li>{@link Dataset#getValue}</li>
|
|
* <li>{@link Dataset#setValue}</li>
|
|
* </ul>
|
|
* Using value formats configured in the Dataset, the methods return formatted value and sets parsed value.
|
|
* <pre><code>let dataset = new Dataset({
|
|
* formats: {
|
|
* numericProperty: {@link numberFormat},
|
|
* customProperty: {
|
|
* format(value) {...},
|
|
* parse(value) {...},
|
|
* }
|
|
* },
|
|
* ...
|
|
* });</code></pre>
|
|
* </p>
|
|
* <p>Working with user data that a Dataset holds, you change the Dataset's state.
|
|
* Depending on the type of change, the Dataset calls back approriate methods.
|
|
* By default, the methods log the content of the change.
|
|
* </p>
|
|
* <p>To override the behavior of the callback methods,
|
|
* define a function with the same signature as the method to override
|
|
* and assign it to the Dataset's method.
|
|
* <pre><code>let myFunc = obj => {...};
|
|
* let dataset = new Dataset({...});
|
|
* dataset.onDatasetChange = myFunc;</code></pre>
|
|
* You can make it simple like this:
|
|
* <pre><code>let dataset = new Dataset({...});
|
|
* dataset.onDatasetChange = obj => {};</code></pre>
|
|
* </p>
|
|
* <p>Or you specify an overriding function in the configuration used to create a Dataset.
|
|
* <pre><code>let dataset = new Dataset({
|
|
* ...
|
|
* onDatasetChange:obj => {...}
|
|
* });</code></pre>
|
|
* </p>
|
|
*/
|
|
class Dataset {
|
|
/**Creates a new Dataset with a configuration.
|
|
* The configuration is an object with which you specify
|
|
* <ul> <li>keymapper - function that returns a key of a user data. Used to identify user data in the Dataset. Mandatory.</li>
|
|
* <li>dataGetter - function that returns an array of user data from an object. Required if the user data are extracted from an object</li>
|
|
* <li>formats - an object of key-value format pairs where the key corresponds to a property of a user data</li>
|
|
* <li>functions called back on a Dataset's events of
|
|
* <ul><li>{@link Dataset#onDatasetChange onDatasetChange}</li>
|
|
* <li>{@link Dataset#onCurrentChange onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange onSelectionChange}</li>
|
|
* <li>{@link Dataset#onAppend onAppend}</li>
|
|
* <li>{@link Dataset#onModify onModify}</li>
|
|
* <li>{@link Dataset#onReplace onReplace}</li>
|
|
* <li>{@link Dataset#onRemove onRemove}</li>
|
|
* <li>{@link Dataset#onDirtiesChange onDirtiesChange}</li>
|
|
* </ul>
|
|
* </li>
|
|
* <li>trace - true to enable message logging</li>
|
|
* </ul>
|
|
* @param conf {object} configuration
|
|
*/
|
|
constructor(conf = {}) {
|
|
this._items = [];
|
|
this.keys = conf.keys || [];
|
|
this._byKeys = {};
|
|
this._current = null;
|
|
|
|
this.conf = notEmpty(conf, "conf is required but missing");
|
|
let keymapper = conf.keymapper;
|
|
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: ""};
|
|
|
|
if (!conf.trace)
|
|
this.log = () => {};
|
|
|
|
|
|
[ "onDatasetChange",
|
|
"onCurrentChange",
|
|
"onSelectionChange",
|
|
"onAppend",
|
|
"onModify",
|
|
"onReplace",
|
|
"onRemove",
|
|
"onDirtiesChange",
|
|
"onSort"
|
|
].forEach(on => {
|
|
let handler = conf[on]
|
|
if (handler && "function" == typeof handler)
|
|
this[on] = handler;
|
|
});
|
|
}
|
|
|
|
/**Logs a message to the console.
|
|
* @param args arguments to log
|
|
*/
|
|
log(...args) {
|
|
console.log.apply(console, args);
|
|
}
|
|
|
|
/**Returns the key 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(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.
|
|
* @param {string} status option regarding the Dataset's user data
|
|
* <ul> <li>none to get keys of all user data</li>
|
|
* <li>"selected" to get keys of selected user data</li>
|
|
* <li>"added" to get keys of added user data</li>
|
|
* <li>"modified" to get keys of modified user data</li>
|
|
* <li>"removed" to get keys of removed user data</li>
|
|
* <li>"dirty" to get keys of user data that are {@link Dataset#dirty dirty}</li>
|
|
* </ul>
|
|
* @returns {array} keys of the Dataset's user data.
|
|
* @example
|
|
* //Other than "dirty" status, keys of user data are returned in an array.
|
|
* let array = dataset.getKeys();
|
|
* array = dataset.getKeys("selected");
|
|
* //With "dirty" status, keys of user data are returned in an object of status-array pairs.
|
|
* let dirties = dataset.getKeys("dirty");
|
|
* let added = dirties.added;
|
|
* let modified = dirties.modified;
|
|
* let removed = dirties.removed;
|
|
*/
|
|
getKeys(status){
|
|
let dataset = this.getDataset(status, "item");
|
|
if ("dirty" != status)
|
|
return dataset.map(e => this.getKey(e));
|
|
|
|
let result = {};
|
|
for (let prop in dataset) {
|
|
result[prop] = dataset[prop].map(e => this.getKey(e));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**Returns user data or dataItem associated with the key.
|
|
* @param {string} key key to a user data
|
|
* @param {string} option "item" to get the user data in a dataItem.
|
|
* @returns {any|DataItem} user data or dataItem associated with the key
|
|
* @example
|
|
* //To get user data associated with a key
|
|
* let data = dataset.getData("key-0");
|
|
* //To get the user data in a dataItem
|
|
* let dataItem = dataset.getData("key-0", "item");
|
|
*/
|
|
getData(key, option) {
|
|
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;
|
|
}
|
|
|
|
/**Sets user data to the Dataset.
|
|
* To get user data from an object, the dataGetter configured is called.
|
|
* After user data is set, the methods
|
|
* <ul> <li>{@link Dataset#onDatasetChange}</li>
|
|
* <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}</li>
|
|
* </ul>
|
|
* are called back.
|
|
* @param {array|object} obj user data or an object that has user data
|
|
* @param {object} obj optional information
|
|
* @returns {Dataset} the Dataset
|
|
*/
|
|
setData(obj, option = {}) {
|
|
this._byKeys = {};
|
|
this._current = null;
|
|
|
|
obj = obj || {};
|
|
|
|
let data = this._getDataItems(obj, option);
|
|
this._items = data.items;
|
|
this._byKeys = data.byKeys;
|
|
this._sorter = {by: ""};
|
|
|
|
this.onDatasetChange(obj, option);
|
|
this.setState(obj.state);
|
|
this.onDirtiesChange(this.dirty);
|
|
|
|
return this;
|
|
}
|
|
|
|
_getDataItems(obj, option) {
|
|
obj = obj || [];
|
|
let array = Array.isArray(obj) ? obj : null;
|
|
if (!array && this.conf.dataGetter)
|
|
array = this.conf.dataGetter(obj) || [];
|
|
array = array || [];
|
|
/*
|
|
let array = Array.isArray(obj) ? obj : this.conf.dataGetter(obj) || [];
|
|
if (!Array.isArray(array))
|
|
throw new Error("The data must be an array");
|
|
*/
|
|
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 {
|
|
items: _items,
|
|
byKeys: _byKeys
|
|
};
|
|
}
|
|
|
|
/**Adds user data to the Dataset.
|
|
* To get user data from an object, the dataGetter configured is called.
|
|
* After user data is set, the methods
|
|
* <ul> <li>{@link Dataset#onDatasetChange}</li>
|
|
* <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}</li>
|
|
* </ul>
|
|
* are called back.
|
|
* @param {array|object} obj user data or an object that has user data
|
|
* @param {object} obj optional information
|
|
* @returns {Dataset} the Dataset
|
|
*/
|
|
addData(obj, option = {}) {
|
|
if (this.empty)
|
|
return this.setData(obj, option);
|
|
|
|
let state = this.state;
|
|
option.added = true;
|
|
let data = this._getDataItems(obj, option);
|
|
this._items = this._items.concat(data.items);
|
|
this._byKeys = {
|
|
...this._byKeys,
|
|
...data.byKeys
|
|
};
|
|
|
|
obj = obj || {}
|
|
|
|
this.onDatasetChange(obj, option);
|
|
this.setState(!Array.isArray(obj) ? obj.state : state);
|
|
this.onDirtiesChange(this.dirty);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**Sorts the Dataset's items by the given property in the given order.
|
|
* @param {string} by name of the Dataset's property
|
|
* @param {boolean}
|
|
* <ul><li>true to sort in ascending order</li>
|
|
* <li>false to sort in descending order</li>
|
|
* </ul>
|
|
* @returns {object} consequent {@link Dataset#sorter sorting status}
|
|
* @example
|
|
* let dataset = ...;
|
|
* //To sort a Dataset's items by the 'prop0' property in ascending order.
|
|
* dataset.sort("prop0", true);
|
|
* //To sort a Dataset's items by the 'prop0' property in descending order.
|
|
* dataset.sort("prop0", false);
|
|
* //To toggle between ascending and descending order
|
|
* dataset.sort("prop0");
|
|
*/
|
|
sort(by, asc) {
|
|
if (this.empty || !by)
|
|
return this.sorter;
|
|
|
|
if (this._sorter.by != by) {
|
|
this._sorter.by = by;
|
|
this._sorter.asc = asc !== undefined ? asc : true;
|
|
} else {
|
|
this._sorter.asc = asc !== undefined ? asc : !this._sorter.asc;
|
|
}
|
|
|
|
let state = this.state;
|
|
|
|
this._items.sort((item0, item1) => {
|
|
let val0 = (item0.data || {})[by],
|
|
val1 = (item1.data || {})[by];
|
|
if (val0 === undefined || val1 === undefined) return 0;
|
|
|
|
if (isEmpty(val0) && isEmpty(val1)) return 0;
|
|
|
|
if (!this._sorter.asc)
|
|
[val0, val1] = [val1, val0];
|
|
|
|
if (!isEmpty(val0)) {
|
|
if ("string" == typeof val0)
|
|
return val0.localeCompare(val1);
|
|
else
|
|
return val0 - val1;
|
|
} else
|
|
return -1;
|
|
});
|
|
|
|
this.setState(state);
|
|
}
|
|
|
|
/**Returns the sorting staus of the Dataset.
|
|
* @returns {object} sorting staus of the Dataset
|
|
*/
|
|
get sorter() {
|
|
let order = "";
|
|
if (this._sorter.asc)
|
|
order = "asc";
|
|
else if (this._sorter.asc === false)
|
|
order = "desc";
|
|
|
|
return {
|
|
by: this._sorter.by,
|
|
order: order
|
|
};
|
|
}
|
|
|
|
/**Clears the Dataset's user data.
|
|
* @returns {Dataset} the Dataset
|
|
*/
|
|
clear() {
|
|
this.setData([]);
|
|
return this;
|
|
}
|
|
|
|
/**Returns the length or count of the Dataset's user data.
|
|
* @returns {number} length or count of the Dataset's user data
|
|
*/
|
|
get length(){return this.getDataset("item").length;};
|
|
|
|
/**Returns whether the Dataset has no user data or not.
|
|
* @returns {boolean}
|
|
* <ul> <li>true if the Dataset has no user data</li>
|
|
* <li>false if the Dataset has any user data</li>
|
|
* </ul>
|
|
*/
|
|
get empty(){
|
|
return this.length < 1;
|
|
}
|
|
|
|
/**Returns the current user data or dataItem.
|
|
* @param {string} option "item" to get the current dataItem
|
|
* @returns {any|DataItem} current user data or dataItem
|
|
* @example
|
|
* //To get the current user data
|
|
* let current = dataset.getCurrent();
|
|
* //To get the current user data in a dataItem
|
|
* let current = dataset.getCurrent("item");
|
|
*/
|
|
getCurrent(option){
|
|
let current = this._current;
|
|
if (!current || current.unreachable)
|
|
return null;
|
|
return "item" == option ? current : current.data;
|
|
}
|
|
|
|
getTempItem(current) {
|
|
let found = this._items.filter(item => item.data._tmpKey || item.isNew());
|
|
found = found.length > 0 ? found[0] : null;
|
|
if (this.keymapped) {
|
|
if (current)
|
|
this.setCurrent();
|
|
} else {
|
|
if (found && current) {
|
|
this._current = found;
|
|
this.onCurrentChange(found);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/**Sets the user data as current that is associated with the key.
|
|
* @param {string} key key to a user data
|
|
* After the data is set, the method
|
|
* <ul> <li>{@link Dataset#onCurrentChange}</li>
|
|
* </ul>
|
|
* is called back.
|
|
*/
|
|
setCurrent(key, fire) {
|
|
let current = this.getCurrent("item"),
|
|
item = this.getData(key, "item") || new DataItem({}, this._formats),
|
|
diff = current !== item;
|
|
|
|
this._current = item;
|
|
|
|
if (diff || fire) {
|
|
this.onCurrentChange(item);
|
|
}
|
|
}
|
|
|
|
/**Returns the Dataset's current state in an object.
|
|
* The object has the properties as follows.
|
|
* <ul> <li>currentKey - key of the current user data</li>
|
|
* <li>selectedKeys - array of keys to the selected user data</li>
|
|
* </ul>
|
|
* @returns {object} Dataset's current state
|
|
*/
|
|
get state() {
|
|
let empty = this.empty,
|
|
self = this;
|
|
|
|
return {
|
|
currentKey:!empty ? self.getKey(self.getCurrent("item")) : null,
|
|
selectedKeys:!empty ? self.getKeys("selected") : []
|
|
};
|
|
}
|
|
|
|
/**Sets the state to the Dataset.
|
|
* After the state is set, the methods
|
|
* <ul> <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* </ul>
|
|
* are called back.
|
|
* @param {object} state state of the Dataset
|
|
* The state is an object of the following properties.
|
|
* <ul> <li>currentKey - key of the current user data</li>
|
|
* <li>selectedKeys - array of keys to the selected user data</li>
|
|
* </ul>
|
|
* @returns {Dataset} the Dataset
|
|
*/
|
|
setState(state) {
|
|
if (this.empty) {
|
|
this.onSort(this.sorter);
|
|
this.onCurrentChange(null);
|
|
this.onSelectionChange([]);
|
|
this.onDirtiesChange(false);
|
|
} else {
|
|
state = state || this.state;
|
|
if (!state.byKeyValue) {
|
|
let current = this.getData(state.currentKey, "item") || this.getDataset("item")[0],
|
|
currentKey = this.getKey(current);
|
|
|
|
this.onSort(this.sorter);
|
|
this.setCurrent(currentKey, true);
|
|
this.select(state.selectedKeys || [], true, true);
|
|
} else {
|
|
let currentKey = this.indexOf(state.currentKey)[0],
|
|
selectedKeys = this.indexOf(...state.selectedKeys);
|
|
|
|
this.onSort(this.sorter);
|
|
this.setCurrent(currentKey, true);
|
|
this.select(selectedKeys, true, true);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**Returns an array of user data or dataItems.
|
|
* @param {string} status
|
|
* <ul> <li>undefined to get all user data</li>
|
|
* <li>"selected" to get selected user data</li>
|
|
* <li>"added" to get added user data</li>
|
|
* <li>"modified" to get modified user data</li>
|
|
* <li>"removed" to get removed user data</li>
|
|
* <li>"dirty" to get dirty user data</li>
|
|
* </ul>
|
|
* @param {string} option
|
|
* <ul> <li>undefined to get array of user data</li>
|
|
* <li>"item" to get array of dataItems</li>
|
|
* </ul>
|
|
* @returns {array|object}
|
|
* <ul> <li>array of user data or dataItems</li>
|
|
* <li>with status of "dirty", object of status and array of user data</li>
|
|
* </ul>
|
|
* @example
|
|
* //To get all user data
|
|
* let dataList = dataset.getDataset();
|
|
* //To get all user data in dataItems
|
|
* let dataItems = dataset.getDataset("item");
|
|
* //To get selected user data
|
|
* let dataList = dataset.getDataset("selected");
|
|
* //To get selected user data in dataItems
|
|
* let dataItems = dataset.getDataset("selected", "item");
|
|
* //To get user data that are either added, modified, or removed
|
|
* let dataList = dataset.getDataset("added");
|
|
* dataList = dataset.getDataset("modified");
|
|
* dataList = dataset.getDataset("removed");
|
|
* //To get user data in dataItems that are either added, modified, or removed.
|
|
* let dataItems = dataset.getDataset("added", "item");
|
|
* dataItems = dataset.getDataset("modified", "item");
|
|
* dataItems = dataset.getDataset("removed", "item");
|
|
* //To get dirty user data
|
|
* let dirties = dataset.getDataset("dirty");
|
|
* let added = dirties.added;
|
|
* let modified = dirties.modified;
|
|
* let removed = dirties.removed;
|
|
* //To get dirty user data in dataItems
|
|
* let dirties = dataset.getDataset("dirty", "item");
|
|
* let added = dirties.added;
|
|
* let modified = dirties.modified;
|
|
* let removed = dirties.removed;
|
|
*/
|
|
getDataset(status, option) {
|
|
let items = this._items,
|
|
result = null;
|
|
if ("item" == status)
|
|
option = "item";
|
|
switch (status) {
|
|
case "selected" : result = items.filter(item => item.selected && !item.unreachable); break;
|
|
case "added":
|
|
case "modified":
|
|
case "removed": result = items.filter(item => status == item.state); break;
|
|
case "dirty":
|
|
result = {};
|
|
items.filter(item => item.dirty)
|
|
.forEach(item => {
|
|
let state = item.state,
|
|
array = result[state];
|
|
if (!array)
|
|
result[state] = array = [];
|
|
array.push(item);
|
|
});
|
|
break;
|
|
case "item":
|
|
default: result = items.filter(item => !item.unreachable); break;
|
|
}
|
|
if ("item" == option)
|
|
return result;
|
|
|
|
let getData = item => item.data;
|
|
if ("dirty" != status)
|
|
return "item" == option ? result : result.map(e => getData(e));
|
|
|
|
for (let prop in result) {
|
|
result[prop] = result[prop].map(e => getData(e));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**Returns whether the Dataset is dirty.
|
|
* A Dataset is dirty if it has user data that is either added, modified, or removed.
|
|
* @returns {boolean} whether the Dataset is dirty
|
|
* <ul> <li>true if the Dataset is dirty</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
get dirty() {
|
|
return this._items
|
|
.filter(item => item.dirty)
|
|
.length > 0;
|
|
}
|
|
|
|
/**Selects or unselects user data depending on the arguments.
|
|
* After the selection change, the method
|
|
* <ul> <li>{@link Dataset#onSelectionChange}</li>
|
|
* </ul>
|
|
* is called.
|
|
* @example
|
|
* //To select a user data
|
|
* dataset.select("key0")</code> or <code>dataset.select("key0", true)
|
|
* //To select multiple user data
|
|
* dataset.select(["key0", "key1"])</code> or <code>dataset.select(["key0", "key1"], true)
|
|
* //To select all user data
|
|
* dataset.select()</code> or <code>dataset.select(true)
|
|
* //To unselect a user data
|
|
* dataset.select("key0", false)
|
|
* //To unselect multiple user data
|
|
* dataset.select(["key0", "key1"], false)
|
|
* //To unselect all user data
|
|
* dataset.select(false)
|
|
*/
|
|
select(...args) {
|
|
let arg0 = ifEmpty(args[0], true),
|
|
arg1 = ifEmpty(args[1], true),
|
|
dirty = false,
|
|
fire = false;
|
|
if ("boolean" == typeof arg0) {
|
|
for (let i = 0, length = this.length; i < length; ++i) {
|
|
dirty = this._items[i].select(arg0) || dirty;
|
|
}
|
|
fire = args[1];
|
|
} else {
|
|
let keys = Array.isArray(arg0) ? arg0 : [arg0];
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
let item = this.getData(keys[i], "item");
|
|
if (item)
|
|
dirty = item.select(arg1) || dirty;
|
|
}
|
|
fire = args[2];
|
|
}
|
|
if (dirty || fire) {
|
|
let selected = this.getDataset("selected", this.keymapped ? undefined : "item");
|
|
this.onSelectionChange(selected);
|
|
}
|
|
return dirty;
|
|
}
|
|
|
|
/**Toggles selection of the user data associated with the key.
|
|
* After the selection change, the method
|
|
* <ul> <li>{@link Dataset#onSelectionChange}</li>
|
|
* </ul>
|
|
* is called.
|
|
* @param {string} key key associated with user data
|
|
* @returns {boolean} selection status of the user data
|
|
* <ul> <li>true if the user data is selected</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
toggle(key) {
|
|
let item = this.getData(key, "item"),
|
|
status = item ? item.toggle() : false,
|
|
selected = this.getDataset("selected", this.keymapped ? undefined : "item");
|
|
|
|
this.onSelectionChange(selected)
|
|
return status;
|
|
}
|
|
|
|
/**appends user data to the Dataset.
|
|
* After the user data is appended, the methods
|
|
* <ul> <li>{@link Dataset#onAppend}</li>
|
|
* <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* @param data {object|array} user data or array of user data
|
|
* @returns the Dataset
|
|
*/
|
|
append(data) {
|
|
if (!data) return this;
|
|
|
|
let found = this.getTempItem(true);
|
|
if (found)
|
|
return found;
|
|
|
|
let notDirty = !this.dirty,
|
|
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);
|
|
|
|
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);
|
|
let last = added[added.length - 1];
|
|
state.currentKey = this.keymapped ? this.getKey(last.data) : last.index;
|
|
this.setState(state);
|
|
|
|
if (notDirty)
|
|
this.onDirtiesChange(true);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**Modifies user data associated with the key using the modifier.
|
|
* After user data modification, the methods
|
|
* <ul> <li>{@link Dataset#onModify}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* @param {string} key key to a Dataset's user data
|
|
* @param {function} modifier function that modifies the user data.
|
|
* The function must have a sigunature that accepts a DataItem.
|
|
* If the modification fails, it must return ValueFormat.InvalidValue.
|
|
* @returns the Dataset
|
|
* @example
|
|
* let v = ...;
|
|
* let modifier = (dataItem) => {
|
|
* if (v !== ...)
|
|
* return ValueFormat.InvalidValue;
|
|
* let data = dataItem.data;
|
|
* data.prop = v;
|
|
* };
|
|
* ...
|
|
* dataset.modify("key0", modifier);
|
|
*/
|
|
modify(key, modifier) {
|
|
if (!modifier) return this;
|
|
|
|
let item = this.getData(key, "item");
|
|
if (!item)
|
|
return log("Item not found with " + key);
|
|
|
|
let notDirty = !this.dirty,
|
|
data = item.data,
|
|
prev = Object.assign({}, data),
|
|
modifiedProps = (prev, data) => {
|
|
let changed = [];
|
|
for (let prop in data) {
|
|
let oldVal = prev[prop],
|
|
newVal = data[prop];
|
|
if (oldVal != newVal)
|
|
changed.push(prop);
|
|
}
|
|
return changed;
|
|
};
|
|
|
|
let current = data == this.getCurrent(),
|
|
revert = modifier(item) == ValueFormat.InvalidValue,
|
|
changed = modifiedProps(prev, data);
|
|
|
|
if (changed.length > 0) {
|
|
if (!item.state)
|
|
item.state = "modified";
|
|
this.onModify(changed, item, current);
|
|
if (notDirty)
|
|
this.onDirtiesChange(true);
|
|
} else if (revert) {
|
|
changed = Object.getOwnPropertyNames(data);
|
|
this.onModify(changed, item, current);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**Replaces the Dataset's user data with the replacement.
|
|
* After replacement, the methods
|
|
* <ul> <li>{@link Dataset#onReplace}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty or not dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* @param {object|array} replacement
|
|
* replacement is an object or an array of objects of the following properties.
|
|
* <ul> <li>key - key to the user data to be replaced</li>
|
|
* <li>data - new user data</li>
|
|
* </ul>
|
|
* @returns {Dataset} the Dataset
|
|
* @example
|
|
* //To replace old-keyed user data with new-keyed user data.
|
|
* dataset.replace({key:"old-key", data:{id:"new-key", ...}});
|
|
* //or
|
|
* dataset.replace([
|
|
* {key:"old-key0", data:{id:"new-key0", ...}},
|
|
* {key:"old-key1", data:{id:"new-key1", ...}},
|
|
* ]);
|
|
* //To replace user data with equal-keyed user data
|
|
* dataset.replace({data:{id:"key0", ...}});
|
|
* //or
|
|
* dataset.replace([
|
|
* {data:{id:"key0", ...}},
|
|
* {data:{id:"key1", ...}},
|
|
* ]);
|
|
*/
|
|
replace(replacement) {
|
|
if (isEmpty(replacement)) return this;
|
|
|
|
let replacements = Array.isArray(replacement) ? replacement : [replacement],
|
|
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 = getKey(obj);
|
|
if (!key) return;
|
|
|
|
let item = getItem(key);
|
|
if (!item) return;
|
|
|
|
item.replace(data);
|
|
replacing.push(item);
|
|
});
|
|
this.onReplace(replacing);
|
|
this.setState();
|
|
return this;
|
|
}
|
|
|
|
/**Removes user data associated with the key.
|
|
* After user data removal, the methods
|
|
* <ul> <li>{@link Dataset#onRemove}</li>
|
|
* <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty or not dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* @param {string|array} key key or keys to user data
|
|
* @returns {Dataset} the Dataset
|
|
* @example
|
|
* dataset.remove("key0");
|
|
* dataset.remove(["key0", "key1"]);
|
|
*/
|
|
remove(key) {
|
|
if (!key || this.empty) return this;
|
|
|
|
let before = this.dirty,
|
|
keys = Array.isArray(key) ? key : [key],
|
|
removed = this._items.filter(item => {
|
|
let k = this.getKey(this.keymapped ? item.data : item),
|
|
remove = keys.includes(k);
|
|
if (remove) {
|
|
item.state = "added" == item.state ? "ignore" : "removed";
|
|
}
|
|
return remove;
|
|
}),
|
|
currentPos = this._items.indexOf(this._current),
|
|
state = this.state;
|
|
|
|
if (currentPos > -1) {
|
|
let newKey = null;
|
|
for (let i = currentPos, length = this._items.length; i < length; ++i) {
|
|
let item = this._items[i];
|
|
if (item.unreachable) continue;
|
|
|
|
newKey = this.getKey(item);
|
|
break;
|
|
}
|
|
if (!newKey)
|
|
for (let i = this._items.length - 1; i > 0; --i) {
|
|
let item = this._items[i];
|
|
if (item.unreachable) continue;
|
|
|
|
newKey = this.getKey(item);
|
|
break;
|
|
}
|
|
state.currentKey = newKey;
|
|
}
|
|
this.onRemove(removed);
|
|
this.setState(state);
|
|
let after = this.dirty;
|
|
if (before != after)
|
|
this.onDirtiesChange(after);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**Erases user data associated with the key.
|
|
* After user data removal, the methods
|
|
* <ul> <li>{@link Dataset#onCurrentChange}</li>
|
|
* <li>{@link Dataset#onSelectionChange}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty or not dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* Note that unlike {@link Dataset#remove} this method deletes user data completely from the Dataset
|
|
* and the erased user data are not traced as dirty user data.
|
|
* @param {string|array} key key or keys to user data
|
|
* @returns {Dataset} the Dataset
|
|
* @example
|
|
* dataset.erase("key0");
|
|
* dataset.erase(["key0", "key1"]);
|
|
*/
|
|
erase(key) {
|
|
if (!key || this.empty) return;
|
|
|
|
let before = this.dirty,
|
|
keys = Array.isArray(key) ? key : [key],
|
|
erased = this._items.filter(item => {
|
|
let k = this.getKey(this.keymapped ? item.data : item),
|
|
erase = keys.indexOf(k) > -1;
|
|
if (erase) {
|
|
delete this._byKeys["key-" + k];
|
|
}
|
|
return erase;
|
|
});
|
|
|
|
let currentPos = erased.indexOf(this._current) > -1 ? this._items.indexOf(this._current) : -1,
|
|
state = this.state;
|
|
|
|
if (currentPos > -1) {
|
|
let newKey = null;
|
|
for (let i = currentPos + 1, length = this._items.length; i < length; ++i) {
|
|
let item = this._items[i];
|
|
if (item.unreachable || erased.includes(item)) continue;
|
|
|
|
newKey = this.getKey(item);
|
|
break;
|
|
}
|
|
if (!newKey)
|
|
for (let i = this._items.length - 1; i > 0; --i) {
|
|
let item = this._items[i];
|
|
if (item.unreachable || erased.includes(item)) continue;
|
|
|
|
newKey = this.getKey(item);
|
|
break;
|
|
}
|
|
state.currentKey = newKey;
|
|
}
|
|
this._items = this._items.filter(function(item){return !erased.includes(item);});
|
|
|
|
this.onRemove(erased);
|
|
this.setState(state);
|
|
let after = this.dirty;
|
|
if (before != after)
|
|
this.onDirtiesChange(after);
|
|
}
|
|
|
|
/**Returns an array of strings converted from the template using the property values of the Dataset's user data.
|
|
* In the template, placeholder for the properties of the user data is specified like {property name}.
|
|
* @param {string} template template string
|
|
* @param {function} formatter function to format a row string with custom property placeholders
|
|
* @returns {array} array of strings converted from the template using the property values of the user data
|
|
*/
|
|
inStrings(template, formatter) {
|
|
let dataset = this.getDataset("item");
|
|
|
|
return dataset.filter(item => !item.data._tmpKey)
|
|
.map((item, index) => item.inString(template, formatter));
|
|
}
|
|
|
|
/**Returns a property value of user data.
|
|
* If a value format is associated with the property, the value is formatted.
|
|
* @param args See the example
|
|
* @returns {any|string} property value of a user data
|
|
* @example
|
|
* //To get a property value of user data associated with a key
|
|
* let value = dataset.getValue("key0", "property0");
|
|
* //To get a property value of current user data
|
|
* let value = dataset.getValue("property0");
|
|
*/
|
|
getValue(...args) {
|
|
let key = null,
|
|
property = null,
|
|
item = null;
|
|
|
|
switch (args.length) {
|
|
case 1:
|
|
//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");
|
|
return item ? item.getValue(property) : undefined;
|
|
}
|
|
|
|
/**Sets a value to a property of user data.
|
|
* If a value format is associated with the property, the value is parsed before setting to user data.
|
|
* After setting the value, the methods
|
|
* <ul> <li>{@link Dataset#onModify}</li>
|
|
* <li>{@link Dataset#onDirtiesChange}(if the Dataset gets dirty)</li>
|
|
* </ul>
|
|
* are called.
|
|
* @param args See the example
|
|
* @example
|
|
* //To set a value to a property of user data associated with a key
|
|
* dataset.setValue("key0", "property0", "value0");
|
|
* //To set a value to a property of current user data
|
|
* dataset.setValue("property0", "value0");
|
|
*/
|
|
setValue(...args) {
|
|
let key = null,
|
|
property = null,
|
|
value = null;
|
|
switch (args.length) {
|
|
case 2:
|
|
key = this.getKey(this.getCurrent(this.keymapped ? undefined : "item"));
|
|
property = args[0];
|
|
value = args[1];
|
|
break;
|
|
case 3:
|
|
key = args[0];
|
|
property = args[1];
|
|
value = args[2];
|
|
break;
|
|
default: return this;
|
|
}
|
|
|
|
return this.modify(key, function(item){
|
|
return item.setValue(property, value);
|
|
});
|
|
}
|
|
|
|
keyValues(...args) {
|
|
return args.map(arg =>
|
|
this.keys.reduce((obj, k) => {
|
|
obj[k] = arg[k];
|
|
return obj;
|
|
}, {})
|
|
);
|
|
}
|
|
|
|
indexOf(...keyValues) {
|
|
let ofKeys = item => {
|
|
let data = item.data;
|
|
|
|
for (let kv of keyValues) {
|
|
let equal = true;
|
|
for (let entry of Object.entries(kv)) {
|
|
let k = entry[0],
|
|
v = entry[1];
|
|
equal = equal && data[k] == v;
|
|
if (!equal)
|
|
break;
|
|
}
|
|
if (equal)
|
|
return equal;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
return this._items
|
|
.filter(item => ofKeys(item))
|
|
.map(item => item.index);
|
|
}
|
|
|
|
/**Called back when user data are set.
|
|
* @param {object|array} obj object that has user data or an array of user data
|
|
* @param {object} obj optional information
|
|
*/
|
|
onDatasetChange(obj, option) {this.log("Dataset changed", obj, "option", option);}
|
|
|
|
/**Called back when current user data is changed.
|
|
* @param {DataItem} currentItem current dataItem
|
|
*/
|
|
onCurrentChange(currentItem) {this.log("Current changed", currentItem);}
|
|
|
|
/**Called back when user data selection changes.
|
|
* @param {array} selected array of selected user data
|
|
*/
|
|
onSelectionChange(selected) {this.log("Selection changed", selected);}
|
|
|
|
/**Called back when user data is appended.
|
|
* @param {object|array} appended user data or array of user data
|
|
*/
|
|
onAppend(appended) {this.log("Data appended", appended);}
|
|
|
|
/**Called back when user data is modified.
|
|
* @param {array} props names of changed properties
|
|
* @param {DataItem} modified modified user dataItem
|
|
* @param {boolean} current whether current user data is modified
|
|
*/
|
|
onModify(props, modified, current) {this.log("Data modified", props, modified, current ? "current" : "");}
|
|
|
|
/**Called back when user data are replaced.
|
|
* @param {array} replacing array of user dataItems replacing the old ones
|
|
*/
|
|
onReplace(replacing) {this.log("Data replaced", replacing);}
|
|
|
|
/**Called back when user data are removed.
|
|
* @param {array} removed array of removed dataItems
|
|
*/
|
|
onRemove(removed) {this.log("Data removed", removed)}
|
|
|
|
/**Called back when the Dataset gets dirty or not dirty.
|
|
* @param {boolean} dirty
|
|
* <ul> <li>true if the Dataset is dirty</li>
|
|
* <li>false otherwise</li>
|
|
* </ul>
|
|
*/
|
|
onDirtiesChange(dirty){this.log("Dirties change", dirty);}
|
|
|
|
/**Handler called back on the sort event.
|
|
* @param {object} status {@link Dataset#sorter sorting status}
|
|
*/
|
|
onSort(status) {this.log("Data sorted", status);}
|
|
}
|
|
|
|
class DatasetControl {
|
|
constructor(conf = {}) {
|
|
this.prefix = conf.prefix;
|
|
this.prefixName = conf.prefixName;
|
|
// this.doctx = conf.doctx || "";
|
|
this.doq = new DomQuery().setContainers(conf.doctx);
|
|
|
|
this.infoSize = conf.infoSize;
|
|
this.appendData = conf.appendData;
|
|
|
|
this.query = {};
|
|
|
|
conf.onDatasetChange = (obj, option) => this.onDatasetChange(obj, option);
|
|
conf.onCurrentChange = item => this.onCurrentChange(item);
|
|
conf.onSelectionChange = selected => this.onSelectionChange(selected);
|
|
conf.onAppend = items => this.onAppend(items);
|
|
conf.onModify = (props, modified, current) => {
|
|
let info = this.dataset.getCurrent("item");
|
|
if (!info) return;
|
|
/*
|
|
if (!info || "added" == info.state)
|
|
return;
|
|
*/
|
|
this.onModify(props, modified, current);
|
|
};
|
|
conf.onDirtiesChange = dirty => this.onDirtiesChange(dirty);
|
|
conf.onReplace = obj => this.onReplace(obj);
|
|
conf.onSort = status => this.onSort(status);
|
|
|
|
this.dataset = new Dataset(conf);
|
|
|
|
this.urls = conf.urls || {
|
|
load:this.url("/list.do"),
|
|
getInfo:this.url("/info.do"),
|
|
create:this.url("/create.do"),
|
|
update:this.url("/update.do"),
|
|
remove:this.url("/remove.do")
|
|
};
|
|
}
|
|
|
|
prefixed(str) {
|
|
return (this.prefix || "") + str;
|
|
}
|
|
|
|
url(str) {
|
|
return wctx.url("/" + this.prefixed(str));
|
|
}
|
|
|
|
load(pageNum) {
|
|
this.query.pageNum = pageNum;
|
|
this._load();
|
|
}
|
|
|
|
_load(option = {}) {
|
|
|
|
if (!this.query.pageNum)
|
|
this.query.pageNum = 1;
|
|
if (option.prev)
|
|
this.query.pageNum = Math.max(1, this.query.pageNum - 1);
|
|
|
|
ajax.get({
|
|
url:this.urls.load,
|
|
data:this.query,
|
|
success:resp => {
|
|
if (!option.prev)
|
|
resp.state = option.state;
|
|
if (!this.appendData || this.query.pageNum == 1)
|
|
this.setData(resp, option);
|
|
else {
|
|
this.addData(resp, option);
|
|
}
|
|
if (option.callback)
|
|
option.callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
reload(option) {
|
|
if (!option)
|
|
option = {};
|
|
option.reloaded = true;
|
|
|
|
let all = option.all,
|
|
prev = option.prev,
|
|
state = this.dataset.keymapped ? this.dataset.state : null;
|
|
if (!state) {
|
|
state = this.empty ? {currentKey: null, selectedKeys: []} : null;
|
|
if (!state) {
|
|
state = {};
|
|
|
|
let current = this.getCurrent(),
|
|
selected = this.getDataset("selected");
|
|
current = this.dataset.keyValues(current);
|
|
selected = this.dataset.keyValues(...selected);
|
|
|
|
state.currentKey = current.length > 0 ? current[0] : null;
|
|
state.selectedKeys = selected;
|
|
state.byKeyValue = true;
|
|
}
|
|
}
|
|
option.state = state;
|
|
|
|
if (!all) {
|
|
this._load(option);
|
|
} else {
|
|
let query = Object.assign({}, this.query);
|
|
this.query.fetchSize = this.dataset.length; //(query.pageNum || 1) * query.fetchSize;
|
|
this.query.pageNum = 1;
|
|
option.callback = () => {this.query = query;};
|
|
this._load(option);
|
|
}
|
|
}
|
|
|
|
download(type) {
|
|
let params = Object.assign({}, this.query);
|
|
params.download = type || "xls";
|
|
|
|
return download.get({
|
|
url: this.urls.load,
|
|
data: params
|
|
});
|
|
}
|
|
|
|
getDataset(option) {
|
|
return this.dataset.getDataset(option);
|
|
}
|
|
|
|
setData(obj, option = {}) {
|
|
this.setPaging(obj, option);
|
|
this.dataset.setData(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);
|
|
}
|
|
|
|
sort(by, asc) {
|
|
this.dataset.sort(by, asc);
|
|
}
|
|
|
|
onDatasetChange(obj, option) {
|
|
debug("onDatasetChange", obj, option);
|
|
}
|
|
|
|
getCurrent(option) {
|
|
return this.dataset.getCurrent(option);
|
|
}
|
|
|
|
setCurrent(key) {
|
|
this.dataset.setCurrent(key);
|
|
}
|
|
|
|
onCurrentChange(item) {
|
|
debug(item);
|
|
}
|
|
|
|
select(key, selected) {
|
|
this.dataset.select(key, selected);
|
|
}
|
|
|
|
onSelectionChange(selected) {
|
|
debug(selected);
|
|
}
|
|
|
|
getInfo(params) {
|
|
if (this.urls.getInfo)
|
|
ajax.get({
|
|
url:this.urls.getInfo,
|
|
data:params || {},
|
|
success:resp => {
|
|
resp = resp.replace(/infoPrefix/g, this.prefix)
|
|
.replace(/prefixName/g, this.prefixName);
|
|
dialog.open({
|
|
id:this.prefixed("dialog"),
|
|
title: this.prefixName + " 정보",
|
|
content:resp,
|
|
size:this.infoSize,
|
|
init:() => {
|
|
let current = this.getCurrent("item");
|
|
this.setInfo(current);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
else
|
|
this.setInfo(this.getCurrent("item"));
|
|
}
|
|
|
|
setInfo(info) {}
|
|
|
|
newInfo(obj) {
|
|
if (this.dataset.keymapped)
|
|
this.dataset.append(obj || {});
|
|
this.getInfo();
|
|
}
|
|
|
|
getValue(name) {
|
|
return this.dataset.getValue(name);
|
|
}
|
|
|
|
setValue(name, value) {
|
|
this.dataset.setValue(name, value);
|
|
}
|
|
|
|
onAppend(items) {
|
|
debug("on append", items);
|
|
}
|
|
|
|
onModify(props, modified, current) {
|
|
debug("on modify", props, "modified", modified, "current", current);
|
|
}
|
|
|
|
onDirtiesChange(dirty) {
|
|
debug("on dirties change", dirty);
|
|
}
|
|
|
|
onReplace(replacing) {
|
|
debug("on replace", replacing);
|
|
}
|
|
|
|
onSort(status) {
|
|
debug("on sort", status);
|
|
}
|
|
|
|
save(info) {
|
|
if (!info) return;
|
|
let item = this.getCurrent("item"),
|
|
create = "added" == item.state;
|
|
|
|
ajax.post({
|
|
url:!create ? this.urls.update : this.urls.create,
|
|
data:info,
|
|
success:resp => this.onSave(resp)
|
|
});
|
|
}
|
|
|
|
onSave(resp) {
|
|
if (resp.saved) {
|
|
dialog.alert("저장됐습니다.");
|
|
dialog.close(this.prefixed("dialog"));
|
|
this.reload();
|
|
}
|
|
}
|
|
|
|
remove(params) {
|
|
let selected = this.dataset.getKeys("selected");
|
|
if (selected.length < 1) return;
|
|
|
|
if (!params) {
|
|
params = {};
|
|
params[this.prefixed("IDs")] = selected.join(",");
|
|
}
|
|
|
|
ajax.post({
|
|
url:this.urls.remove,
|
|
data:params,
|
|
success:resp => this.onRemove(selected, resp)
|
|
});
|
|
}
|
|
|
|
onRemove(selected, resp) {
|
|
if (resp.saved)
|
|
this.reload({prev: selected.length == this.dataset.length});
|
|
}
|
|
|
|
clear() {
|
|
this.dataset.clear();
|
|
}
|
|
|
|
selector(selector) {
|
|
return this.doq.selector(selector);
|
|
}
|
|
|
|
find(...args) {
|
|
return this.doq.find(...args);
|
|
}
|
|
|
|
findAll(...args) {
|
|
return this.doq.findAll(...args);
|
|
}
|
|
} |