js/core/data.js
//= require events
/**
* Источник данных
*
*
* @param {Object} o параметры
* @param {function} o.valueEql функция сравнения (comparator)
* @param {function} o.valueUid функция идентификации (identifier)
* @param {function} o.filter функция фильтрации
* @param {function} o.sorter функция сортировки
*
*
* События:
* @fires Ergo.core.Event#changed
* @fires Ergo.core.Event#diff
* @fires Ergo.core.Event#dirty
*
*
* @class
* @name Ergo.core.DataSource
* @extends Ergo.core.Object
* @mixes observable
*
*/
Ergo.defineClass('Ergo.core.DataSource', /** @lends Ergo.core.DataSource.prototype */{
extends: 'Ergo.core.Object',
mixins: ['observable'],
defaults: {
// plugins: [Ergo.Observable],
// include: 'observable',
lazy: true
},
_initialize: function(src, id, o) {
/**
* Источник данных
*
* @field {Any|Ergo.core.DataSource}
*/
this.source = src;
/**
* Ключ связанных данных в источнике данных
*
* @field _id
*/
if(arguments.length == 2) {
if($.isPlainObject(id)) o = id;
else this._id = id;
}
else if(arguments.length == 3) {
this._id = id;
}
if('_id' in this) {
// if(typeof id == 'string')
// this._id = this._id.split('+');
if(Array.isArray(id)) {
this._id = id;
}
else {
this._id = [this._id];
}
}
// this._super(o || {});
Ergo.core.DataSource.superclass._initialize.call(this, o || {});
var self = this;
var o = this.options;
var val = this.get();
/**
* Элементы данных
*
* @field
*/
this.entries = Array.isArray(val) ? new Ergo.core.Array() : new Ergo.core.Collection();
// this.events = new Ergo.events.Observer(this);
if(!o.lazy) {
$ergo.each(val, function(v, i){ self.entry(i); } );
}
this._bindEvents();
// console.log('-- data --');
},
_destroy: function() {
// var src = this.source;
// this.del();
// очищаем регистрацию обработчиков событий
this.events.off();
// удаляем все дочерние элементы
// this.entries.applyAll('_destroy');
while(this.entries.count()) {
this.entries.last()._destroy()
}
if( this.source instanceof Ergo.core.DataSource ) {
this.source.entries.remove(this);
}
},
/**
* Получение вложенного элемента данных по ключу
*
* Если элемента данных нет, то он будет создан
*
* @param {String|Any} i ключ
* @return {Ergo.core.DataSource} entry элемент данных
*/
entry: function(i) {
// console.log('-- data entry --');
var ds = this;
// multikey
if( this._id && this._id.length > 1 ) {
this._id.forEach(function(id) {
if(id == i) {
ds = ds.source;
}
});
}
// если ключ - строка, то он может быть составным
if( typeof i == 'string' ) {
var a = i.split('.');
var i = a.pop();
// двигаемся внутрь объекта по элементам ключа
for(var j = 0; j < a.length; j++) ds = ds.entry(a[j]);
}
var e = ds.entries.get(i);
if(!e) {
e = ds._factory(i);
ds.entries.set(i, e);
}
return e;
// // если ключ существует, то возвращаем соответствующий элемент, иначе - создаем новый
// if(!ds.entries.has_key(i)) {
// ds.entries.set(i, ds._factory(i));
// }
//
// return ds.entries.get(i);
},
/**
* Фабрика вложенных элементов
*
* По умолчанию используется класс Ergo.core.DataSource
*
* @param {Any} i ключ
* @returns {Ergo.core.DataSource}
*
* @protected
*
*/
_factory: function(i) {
return new Ergo.core.DataSource(this, i);
},
// _parse: null,
// _compose: null,
/**
* Получение значения источника данных
*
* Если метод вызывается без аргументов, то он ведет себя как геттер.
* Если определен аргумент, то метод является сеттером.
*
* @param {Any} [v] значение
* @private
*/
_val: function(v) {
// if('_cached' in this) return this._cached;
// var v = undefined;
if(arguments.length == 0) {
v = (this.source instanceof Ergo.core.DataSource) ? this.source._val() : this.source;
if('_id' in this) {
if(v) {
// single key
if(this._id.length == 1) {
v = v[this._id[0]];
}
// multi key
else {
var mv = {};
for(var i = 0; i < this._id.length; i++)
mv[this._id[i]] = v[this._id[i]];
v = mv;
// var mv = [];
// for(var i = 0; i < this._id.length; i++)
// mv.push( v[this._id[i]] );
// v = mv;
}
}
else {
v = undefined;
}
}
// if(this.options.unformat)
// v = this.options.unformat.call(this, v);
}
else {
// if(this.options.format)
// v = this.options.format.call(this, v);
if (this.source instanceof Ergo.core.DataSource) {
if('_id' in this) {
var src = this.source._val();
// single key
if(this._id.length == 1) {
src[this._id[0]] = v
}
// multi key
else {
for(var i = 0; i < this._id.length; i++) {
var key = this._id[i];
if(key in v)
src[key] = v[key];
}
// for(var i in v){//var i = 0; i < this._id.length; i++) {
// src[this._id[i]] = v[i];
// }
}
}
else {
this.source._val(v);
}
}
else {
if('_id' in this) {
var src = this.source;
// single key
if(this._id.length == 1) {
src[this._id[0]] = v
}
// multi key
else {
for(var i = 0; i < this._id.length; i++) {
var key = this._id[i];
if(key in v)
src[key] = v[key];
}
// for(var i in v) {//var i = 0; i < this._id.length; i++) {
// src[this._id[i]] = v[i];
// }
}
}
else {
this.source = v;
}
}
}
// this._cached = v;
return v;
},
/**
* "Сырое" значение, ассоциированное с источником данных
* @returns {Any}
*/
raw: function() {
return this._val();
},
/**
* Получение значения элемента по ключу
*
* Если ключ не указан или не определен, то берется значение самого источника данных
*
* @param {Number|String} [i] ключ
* @returns {Any}
*/
get: function(i) {
if(i === undefined){
return this._val();
}
else {
return this.entry(i).get();
}
},
/**
* Полная копия "сырых" данных
*
* @param {Number|String} i ключ
* @returns {Any}
*/
rawCopy: function(i) {
return Ergo.deepCopy(this.get(i));
},
/**
* Изменение существующего элемента
*
* Если аргумент один, то изменяется значение самого источника данных
*
* @emits Ergo.core.Event#changed
* @emits Ergo.core.Event#diff
*
* @param {String|Number} [i] ключ
* @param {Any} val новое значение
*
*/
set: function(i, newValue) {
if(arguments.length == 1) {
newValue = i;
var oldValue = this.get();
// var filtered = [];
// this.entries.each(function(e) {
// //FIXME упрощенная проверка присутствия ключа
// if(newValue && newValue[e.id] === undefined)
// filtered.push(e);
// });
// for(var i = 0; i < filtered.length; i++) {
// filtered[i]._destroy();
// }
this.entries
.filter(function(e){
// if( newValue ) {
// var n = 0;
// for(var i = 0; i < e._id.length; i++) {
// if( newValue[e._id[i]] === undefined ) {
// n++;
// }
// }
// return n == e._id.length;
// }
// return false;
//FIXME упрощенная проверка присутствия ключа
// плюс к этому нельзя удалять источник данных, если он связан с чем-то событиями
return (newValue && newValue[e._id.join('+')] === undefined) && Object.keys(e.events.events).length == 0 ;
})
.each(function(e){
e._destroy();
});
if(this.entries.isEmpty())
this.entries = Array.isArray(newValue) ? new Ergo.core.Array() : new Ergo.core.Collection();
// удаляем все элементы
// this.entries.filter(function(e) { return true; }).each(function(e){ e._destroy(); });
// пересоздаем коллекцию элементов
// положительный эффект в том, что можно поменять содержимое Object на Array и наоборот
// this.entries = $.isArray(newValue) ? new Ergo.core.Array() : new Ergo.core.Collection();
// var entries_to__destroy = [];
//
// this.entries.each(function(e){
// // //FIXME упрощенная проверка присутствия ключа
// // if(newValue[e.id] === undefined) entries_to__destroy.push(e);
// entries_to__destroy.push(e);
// });
//
// for(var i = 0; i < entries_to__destroy.length; i++)
// entries_to__destroy[i]._destroy();
// опустошаем список элементов
// this.entries.applyAll('_destroy');
this._val(newValue);
// var ds = this.source;
// while(ds) {
// ds.events.fire('entry:changed', {entry: this});
// ds = ds.source;
// }
if(this.source instanceof Ergo.core.DataSource) {
// this.source.events.fire('entry:changed', {entry: this, changed: [this]});
if(!this.source._no_diff) {
this.source.events.fire('diff', {updated: [this]});
// this.mark_dirty();
}
}
this.events.fire('changed', {'oldValue': oldValue, 'newValue': newValue});
// console.log('set', oldValue, newValue);
if(this.source instanceof Ergo.core.DataSource) {
// this.source.events.fire('entry:changed', {entry: this, changed: [this]});
if(!this.source._no_diff) {
// this.source.events.fire('diff', {updated: [this]});
this.mark_dirty();
}
}
// var self = this;
//
// // FIXME
// if(this.source instanceof Ergo.core.DataSource) {
// this.source.entries.each(function(e) {
// e._id.forEach(function(id) {
// if(id == self._id[0])
// e.events.fire('changed', {'oldValue': oldValue, 'newValue': newValue});
// })
// });
// }
// this._changed = true;
return this;
}
else {
return this.entry(i).set(newValue);
}
},
/**
* Добавление нового элемента
*
* @emits Ergo.core.Event#changed
* @emits Ergo.core.Event#diff
*
* @param {Any} value значение нового атрибута
* @param {String|Number} [index] индекс или ключ, куда должен быть добавлен атрибут
* @returns {Ergo.core.DataSource}
*/
add: function(value, index) {
var values = this.get();
var isLast = false;
if(Array.isArray(values)) {
if(index == null){
values.push(value);
index = values.length-1;
isLast = true;
}
else {
// меняем индексы элементов данных
for(var i = values.length-1; i >= index; i--){
var e = this.entries.get(i);
// this.events.fire('onIndexChanged', {'oldIndex': j, 'newIndex': (j-1)});
if(e)
e._id[0] = i+1;
this.entries.set(i+1, e);
}
// добавляем новый элемент массива
values.splice(index, 0, value);
this.entries.set(index, this._factory(index));
}
}
else {
// throw new Error('Method "add" does not support object src');
values[index] = value;
}
var e = this.entry(index);
// this.events.fire('entry:added', {'index': isLast ? undefined : index, entry: e});//, 'isLast': isLast});
if(!this._no_diff) {
this.events.fire('diff', {created: [e]});
this.mark_dirty(true);
}
// e.events.fire('changed', {created: [e]}); // ?
return e;
},
/**
* Удаление элемента.
*
* Если метод вызывается без аргументов, то удаляется сам источник данных из родительского
*
* @emits Ergo.core.Event#diff
*
* @param {String|Number} [i] ключ
*
*/
del: function(i) {
if(arguments.length == 1) {
this.entry(i).del();
}
else {
// this.events.fire('changed', {/*'oldValue': deleted_value, 'newValue': undefined,*/ deleted: [this]});
var src = this.source;
var value = src;
if( src instanceof Ergo.core.DataSource ) {
value = src._val();
}
var deleted_value = this._val(); //value ? value[this._id[0]] : undefined;
if(Array.isArray(value)) {
value.splice(this._id[0], 1);
if( src instanceof Ergo.core.DataSource ) {
for(var j = this._id[0]; j < value.length; j++) {
var e = src.entries.get(j+1);
if(e)
e._id[0] = j
}
}
}
else {
if(value) {
for(var j = 0; j < this._id.length; j++)
delete value[this._id[j]];
}
}
if( this.source instanceof Ergo.core.DataSource ) {
src.entries.remove(this);
if(!this.source._no_diff) {
// src.events.fire('entry:deleted', {'entry': this, 'value': deleted_value});
this.source.events.fire('diff', {deleted: [this]});
this.source.mark_dirty(true);
}
}
}
},
/**
* Удаление элемента по значению.
*
* @param {Any} [v] значение элемента
*
*/
rm: function(v) {
var val = this._val();
var k = null;
var criteria = JSON.stringify(v);
Ergo.find(val, function(obj, i) {
if( JSON.stringify(obj) === criteria ) {
k = i;
return true; // прекращаем обход
}
});
if( k != null) {
this.del(k);
}
},
/**
* Последовательный обход всех вложенных элементов с поддержкой фильтрации и сортировки
*
* @param {Function} filter фильтр
* @param {Function} sorter сортировка
* @param {Function} pager страница
* @param {Function} callback
*
*/
stream: function(filter, sorter, pager, callback) {
var ds = this;
var values = ds.get();
// var keys = this.keys(this.options.filter);
var filter = filter || ds.options.filter;
var sorter = sorter || ds.options.sorter;
if( filter || sorter ) {
var kv_a = [];
// Filtering source and mapping it to KV-array
Ergo.each(values, function(v, i) {
if(!filter || filter(v, i)) {
kv_a.push( [i, v] );
// callback.call(self, self.entry(i), i, v);
}
});
if(sorter) {
// Sorting KV-array
kv_a.sort( sorter );
}
for(var i = 0; i < kv_a.length; i++) {
var kv = kv_a[i];
callback.call(ds, ds.entry(kv[0]), kv[0], kv[1]);
}
}
else {
Ergo.each(values, function(v, i) {
callback.call(ds, ds.entry(i), i, v);
});
}
// var keys = [];
// if($.isArray(values)) {
// for(var i = 0; i < values.length; i++) keys.push(i);
// }
// else {
// for(var i in values) keys.push(i);
// }
//TODO здесь могут применяться модификаторы списка ключей (сортировка, фильтрация)
// if(this.options.filter){
// keys = this.options.filter.call(this, values, keys);
// }
// for(var i = 0; i < keys.length; i++){
// var k = keys[i];
// callback.call(this, this.entry(k), k, values[k]);
// }
},
/**
* Обновление данных с синхронизацией
*
* Определяется разница между новыми и старыми данными, формируется diff-объект и
* генерируется событие `diff`
*
* @emits Ergo.core.Event#diff
*
* @params {Any} newData новые данные
*
*/
sync: function(newData) {
var self = this;
var valueUid = (this.options.valueUid || this._valueUid);
var valueEql = (this.options.valueEql || this._valueEql);
var oldData = this._val();
var diff = {
created: [],
deleted: [],
updated: []
}
// console.log('sync', oldData, newData);
this._no_diff = true;
// for arrays
if( Array.isArray(oldData) && Array.isArray(newData) ) {
var value_m = {};
for(var i = 0; i < newData.length; i++) {
//TODO способ получения ключа может быть сложнее
var v = newData[i];
var k = valueUid.call(this, v, i);
// if(this.options.idKey) {
// value_m[newData[i][this.options.idKey]] = {value: newData[i], index: i};
// }
// else {
value_m[k] = {value: newData[i], index: i};
// }
}
// console.log('sync', JSON.stringify(value_m));
for(var i = 0; i < oldData.length; i++) {
var v = oldData[i];
var k = valueUid.call(this, v, i);
// var k = (this.options.idKey) ? v[this.options.idKey] : i;
if( k in value_m ) {
delete value_m[k];
}
else {
// DELETE
diff.deleted.push( this.entry(i) );
}
}
for(var i = diff.deleted.length-1; i >= 0; i-- ) {
diff.deleted[i].del();
}
Object.keys(value_m)
.map(function(k) { return value_m[k] })
.sort(function(a,b) { return a.index-b.index; })
.forEach(function(val) {
var v = val.value;
var i = val.index;
// CREATE
diff.created.push( self.add(v, i) );
});
this._val( newData );
for(var i = 0; i < newData.length; i++) {
if( !valueEql(oldData[i], newData[i]) ) {
// if( JSON.stringify(oldData[i]) !== JSON.stringify(newData[i]) ) {
diff.updated.push( this.entry(i) );
}
}
}
// for objects
else {
var value_m = {};
// строим ассоциацию uid-ов и значений
for(var i in newData) {
var v = newData[i];
var k = valueUid.call(this, v, i);
value_m[k] = {value: newData[i], index: i};
}
// составляем список uid-ов для удаления
for(var i in oldData) {
var v = oldData[i];
var k = valueUid.call(this, v, i);
if( k in value_m ) {
delete value_m[k];
}
else {
// DELETE
diff.deleted.push( this.entry(i) );
}
}
// удаляем элементы данных
for(var i = diff.deleted.length-1; i >= 0; i-- ) {
diff.deleted[i].del();
}
// добавляем новые элементы данных
Object.keys(value_m)
.map(function(k) { return value_m[k] })
.forEach(function(val) {
var v = val.value;
var i = val.index;
// CREATE
diff.created.push( self.set(i, v) );
});
// обновляем значение
this._val( newData );
// различающиеся элементы помечаем для обновления
for(var i in newData) {
if( !valueEql(oldData[i], newData[i]) ) {
diff.updated.push( this.entry(i) );
}
}
}
// if(diff.updated.length != 0) {
// }
this._no_diff = false;
// console.log('sync diff', diff, oldData, newData);
this.events.fire('diff', diff);
// for(var i = diff.created.length-1; i >= 0; i-- ) {
// diff.created[i].events.fire('changed');
// }
for(var i = diff.updated.length-1; i >= 0; i-- ) {
diff.updated[i].events.fire('changed');
}
this.mark_dirty(true);
},
/**
* Функция сравнения (comparator) двух значений A и B
*
* @param {Any} a значение A
* @param {Any} b значение B
* @return {boolean} значения равны
*/
_valueEql: function(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
},
/**
* Функция идентификатора (identifier) значения
*
* @param {Any} v значение
* @param {String|Integer} i ключ значения в источнике данных
* @return {Any} уникальный идентификатор
*/
_valueUid: function(v, i) {
return i;
},
// diff: function(difference, filter, sorter) {
//
// var ds = this;
//
// // DELETED
// for(var i = 0; i < difference.deleted.length; i++) {
// var d = difference.created[i];
// this.events.fire('entry:deleted', {entry: d.entry});
// }
//
// // CREATED
// for(var i = 0; i < difference.created.length; i++) {
// var d = difference.created[i];
// if(!filter || filter.call(this, d.value, d.id)) {
// var index = d.id;
// if(sorter) {
// // FIXME поменять поиск на бинарный
// }
// }
// }
//
//
//
//
// },
/*
keys: function(criteria) {
var keys = [];
var values = this.get();
Ergo.each(function(v, i){
if(criteria || criteria.call(this, v, i)) keys.push(i);
});
return keys;
// if($.isArray(values)) {
// for(var i = 0; i < values.length; i++) keys.push(i);
// }
// else {
// for(var i in values) keys.push(i);
// }
// return keys
},
*/
mark_dirty: function(do_event, e) {
this._changed = true;
if(this.source && this.source instanceof Ergo.core.DataSource) {// && !this.source._changed)
var self = this;
this.source.entries.each(function(src_entry) {
if(src_entry != self && src_entry._id.length > 1) {
src_entry._id.forEach(function(id) {
// FIXME
if(id == self._id[0]) {
src_entry.events.fire('dirty', e || {});
}
})
}
})
this.source.mark_dirty(true, {updated: [this]});
}
if(do_event)
this.events.fire('dirty', e || {});
},
walk: function(callback) {
//TODO
},
/**
* Проверка, изменялся ли источник данных или хотя бы один из его атрибутов/элементов
*
* @returns {Boolean}
*/
changed: function() {
if(this._changed) return true;
var found = this.entries.find(function(e){ return e.changed(); });
return found != null;
},
/*
* Удаление состояния о том, что источник данных или его атрибуты изменялись
*/
clean: function() {
this._changed = false;
this.entries.applyAll('clean');
},
/**
* Количество элементов "сырых" данных
*
* @returns {Number}
*/
size: function() {
return $ergo.size(this._val());
}
});
//Ergo.merge(Ergo.core.DataSource.prototype, Ergo.alias('mixins:observable'));
/**
* Пространство имен для данных
*
* @namespace Ergo.data
*/
Ergo.data = {};
Ergo.$data = Ergo.object;