mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
|
var _ = require('underscore');
|
||
|
var assert = require('assert');
|
||
|
var BackboneEvents = require('backbone').Events;
|
||
|
|
||
|
// Common
|
||
|
var gutil = require('app/common/gutil');
|
||
|
|
||
|
// Libraries
|
||
|
var dispose = require('../lib/dispose');
|
||
|
var koArray = require('../lib/koArray');
|
||
|
|
||
|
// Models
|
||
|
var rowset = require('./rowset');
|
||
|
var TableModel = require('./TableModel');
|
||
|
var {DataRowModel} = require('./DataRowModel');
|
||
|
const {TableQuerySets} = require('./QuerySet');
|
||
|
|
||
|
/**
|
||
|
* DataTableModel maintains the model for an arbitrary data table of a Grist document.
|
||
|
*/
|
||
|
function DataTableModel(docModel, tableData, tableMetaRow) {
|
||
|
TableModel.call(this, docModel, tableData);
|
||
|
|
||
|
this.tableMetaRow = tableMetaRow;
|
||
|
|
||
|
this.tableQuerySets = new TableQuerySets(this.tableData);
|
||
|
|
||
|
// New RowModels are created by copying fields from this._newRowModel template. This way we can
|
||
|
// update the template on schema changes in the same way we update individual RowModels.
|
||
|
// Note that tableMetaRow is incomplete when we get a new table, so we don't rely on it here.
|
||
|
var fields = tableData.getColIds();
|
||
|
assert(fields.includes('id'), "Expecting tableData columns to include `id`");
|
||
|
|
||
|
// This row model gets schema actions via rowNotify, and is used as a template for new rows.
|
||
|
this._newRowModel = this.autoDispose(new DataRowModel(this, fields));
|
||
|
|
||
|
// TODO: Disposed rows should be removed from the set.
|
||
|
this._floatingRows = new Set();
|
||
|
|
||
|
// Listen for notifications that affect all rows, and apply them to the template row.
|
||
|
this.listenTo(this, 'rowNotify', function(rows, action) {
|
||
|
// TODO: (Important) Updates which affect a subset of rows should be handled more efficiently
|
||
|
// for _floatingRows.
|
||
|
// Ideally this._floatingRows would be a Map from rowId to RowModel, like in the LazyArrayModel.
|
||
|
if (rows === rowset.ALL) {
|
||
|
this._newRowModel.dispatchAction(action);
|
||
|
this._floatingRows.forEach(row => {
|
||
|
row.dispatchAction(action);
|
||
|
});
|
||
|
} else {
|
||
|
this._floatingRows.forEach(row => {
|
||
|
if (rows.includes(row.getRowId())) { row.dispatchAction(action); }
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// TODO: In the future, we may need RowModel to support fields such as SubRecordList, containing
|
||
|
// collections of records from another table (probably using RowGroupings as in MetaTableModel).
|
||
|
// We'll need to pay attention to col.type() for that.
|
||
|
}
|
||
|
|
||
|
dispose.makeDisposable(DataTableModel);
|
||
|
_.extend(DataTableModel.prototype, TableModel.prototype);
|
||
|
|
||
|
/**
|
||
|
* Creates and returns a LazyArrayModel of RowModels for the rows in the given sortedRowSet.
|
||
|
* @param {Function} optRowModelClass: Class to use for a RowModel in place of DataRowModel.
|
||
|
*/
|
||
|
DataTableModel.prototype.createLazyRowsModel = function(sortedRowSet, optRowModelClass) {
|
||
|
var RowModelClass = optRowModelClass || DataRowModel;
|
||
|
var self = this;
|
||
|
return new LazyArrayModel(sortedRowSet, function makeRowModel() {
|
||
|
return new RowModelClass(self, self._newRowModel._fields);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns a new rowModel created using `optRowModelClass` or default `DataRowModel`.
|
||
|
* It is the caller's responsibility to dispose of the returned rowModel.
|
||
|
*/
|
||
|
DataTableModel.prototype.createFloatingRowModel = function(optRowModelClass) {
|
||
|
var RowModelClass = optRowModelClass || DataRowModel;
|
||
|
var model = new RowModelClass(this, this._newRowModel._fields);
|
||
|
this._floatingRows.add(model);
|
||
|
model.autoDisposeCallback(() => {
|
||
|
this._floatingRows.delete(model);
|
||
|
});
|
||
|
return model;
|
||
|
};
|
||
|
|
||
|
//----------------------------------------------------------------------
|
||
|
|
||
|
/**
|
||
|
* LazyArrayModel inherits from koArray, and stays parallel to sortedRowSet.getKoArray(),
|
||
|
* maintaining RowModels for only *some* items, with nulls for the rest.
|
||
|
*
|
||
|
* It's tailored for use with koDomScrolly.
|
||
|
*
|
||
|
* You must not modify LazyArrayModel, but are free to use non-modifying koArray methods on it.
|
||
|
* It also exposes methods:
|
||
|
* makeItemModel()
|
||
|
* setItemModel(rowModel, index)
|
||
|
* And it takes responsibility for maintaining
|
||
|
* rowModel._index() - An observable equal to the current index of this item in the array.
|
||
|
*
|
||
|
* @param {rowset.SortedRowSet} sortedRowSet: SortedRowSet to mirror.
|
||
|
* @param {Function} makeRowModelFunc: A function that creates and returns a DataRowModel.
|
||
|
*
|
||
|
* @event rowModelNotify(rowModels, action):
|
||
|
* Forwards the action from 'rowNotify' event, but with a list of affected RowModels rather
|
||
|
* than a list of affected rowIds. Only instantiated RowModels are included.
|
||
|
*/
|
||
|
function LazyArrayModel(sortedRowSet, makeRowModelFunc) {
|
||
|
// The underlying koArray contains some rowModels, and nulls for other elements. We keep it in
|
||
|
// sync with rowIdArray. First, initialize a koArray of proper length with all nulls.
|
||
|
koArray.KoArray.call(this, sortedRowSet.getKoArray().peek().map(function(r) { return null; }));
|
||
|
this._rowIdArray = sortedRowSet.getKoArray();
|
||
|
this._makeRowModel = makeRowModelFunc;
|
||
|
|
||
|
this._assignedRowModels = new Map(); // Assigned rowModels by rowId.
|
||
|
this._allRowModels = new Set(); // All instantiated rowModels.
|
||
|
|
||
|
this.autoDispose(this._rowIdArray.subscribe(this._onSpliceChange, this, 'spliceChange'));
|
||
|
this.listenTo(sortedRowSet, 'rowNotify', this.onRowNotify);
|
||
|
|
||
|
// On disposal, dispose each instantiated RowModel.
|
||
|
this.autoDisposeCallback(function() {
|
||
|
for (let r of this._allRowModels) {
|
||
|
// TODO: Ideally, row models should be disposable.
|
||
|
if (typeof r.dispose === 'function') {
|
||
|
r.dispose();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* LazyArrayModel inherits from koArray.
|
||
|
*/
|
||
|
LazyArrayModel.prototype = Object.create(koArray.KoArray.prototype);
|
||
|
dispose.makeDisposable(LazyArrayModel);
|
||
|
_.extend(LazyArrayModel.prototype, BackboneEvents);
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Returns a new item model, as needed by setItemModel(). It is the only way for a new item
|
||
|
* model to get instantiated.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.makeItemModel = function() {
|
||
|
var rowModel = this._makeRowModel();
|
||
|
this._allRowModels.add(rowModel);
|
||
|
return rowModel;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Unassigns a given rowModel, removing it from the LazyArrayModel.
|
||
|
* @returns {Boolean} True if rowModel got unset, false if it was already unset.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.unsetItemModel = function(rowModel) {
|
||
|
this.setItemModel(rowModel, null);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Assigns a given rowModel to the given index. If the rowModel was previously assigned to a
|
||
|
* different index, the old index reverts to null. If index is null, unsets the rowModel.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.setItemModel = function(rowModel, index) {
|
||
|
var arr = this.peek();
|
||
|
|
||
|
// Remove the rowModel from its old index in the observable array, and in _assignedRowModels.
|
||
|
var oldIndex = rowModel._index.peek();
|
||
|
if (oldIndex !== null && arr[oldIndex] === rowModel) {
|
||
|
arr[oldIndex] = null;
|
||
|
}
|
||
|
if (rowModel._rowId !== null) {
|
||
|
this._assignedRowModels.delete(rowModel._rowId);
|
||
|
}
|
||
|
|
||
|
// Handles logic to set the rowModel to the given index.
|
||
|
this._setItemModel(rowModel, index);
|
||
|
|
||
|
if (index !== null && arr.length !== 0) {
|
||
|
// Ensure that index is in-range.
|
||
|
index = gutil.clamp(index, 0, arr.length - 1);
|
||
|
|
||
|
// If there is already a model at the destination index, unassign that one.
|
||
|
if (arr[index] !== null && arr[index] !== rowModel) {
|
||
|
this.unsetItemModel(arr[index]);
|
||
|
}
|
||
|
|
||
|
// Add the newly-assigned model in its place in the array and in _assignedRowModels.
|
||
|
arr[index] = rowModel;
|
||
|
this._assignedRowModels.set(rowModel._rowId, rowModel);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Assigns a given floating rowModel to the given index.
|
||
|
* If index is null, unsets the floating rowModel.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.setFloatingRowModel = function(rowModel, index) {
|
||
|
this._setItemModel(rowModel, index);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Helper function to assign a given rowModel to the given index. Used by setItemModel
|
||
|
* and setFloatingRowModel. Does not interact with the array, only the model itself.
|
||
|
*/
|
||
|
LazyArrayModel.prototype._setItemModel = function(rowModel, index) {
|
||
|
var arr = this.peek();
|
||
|
|
||
|
if (index === null || arr.length === 0) {
|
||
|
// Unassign the rowModel if index is null or if there is no valid place to assign it to.
|
||
|
rowModel._index(null);
|
||
|
rowModel.assign(null);
|
||
|
} else {
|
||
|
// Otherwise, ensure that index is in-range.
|
||
|
index = gutil.clamp(index, 0, arr.length - 1);
|
||
|
|
||
|
// Assign the rowModel and set its index.
|
||
|
rowModel._index(index);
|
||
|
rowModel.assign(this._rowIdArray.peek()[index]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called for any updates to rows, including schema changes. This may affect some or all of the
|
||
|
* rows; in the latter case, rows will be the constant rowset.ALL.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.onRowNotify = function(rows, action) {
|
||
|
if (rows === rowset.ALL) {
|
||
|
for (let rowModel of this._allRowModels) {
|
||
|
rowModel.dispatchAction(action);
|
||
|
}
|
||
|
this.trigger('rowModelNotify', this._allRowModels);
|
||
|
} else {
|
||
|
var affectedRowModels = [];
|
||
|
for (let r of rows) {
|
||
|
var rowModel = this._assignedRowModels.get(r);
|
||
|
if (rowModel) {
|
||
|
rowModel.dispatchAction(action);
|
||
|
affectedRowModels.push(rowModel);
|
||
|
}
|
||
|
}
|
||
|
this.trigger('rowModelNotify', affectedRowModels);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Internal helper called on any change in the underlying _rowIdArray. We mirror each new rowId
|
||
|
* with a null. Removed rows are unassigned. We also update subsequent indices.
|
||
|
*/
|
||
|
LazyArrayModel.prototype._onSpliceChange = function(splice) {
|
||
|
var numDeleted = splice.deleted.length;
|
||
|
var i, n;
|
||
|
|
||
|
// Unassign deleted models, and leave for the garbage collector to find.
|
||
|
var arr = this.peek();
|
||
|
for (i = splice.start, n = 0; n < numDeleted; i++, n++) {
|
||
|
if (arr[i]) {
|
||
|
this.unsetItemModel(arr[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Update indices for other affected elements.
|
||
|
var delta = splice.added - numDeleted;
|
||
|
if (delta !== 0) {
|
||
|
var firstToAdjust = splice.start + numDeleted;
|
||
|
for (let rowModel of this._assignedRowModels.values()) {
|
||
|
var index = rowModel._index.peek();
|
||
|
if (index >= firstToAdjust) {
|
||
|
rowModel._index(index + delta);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Construct the arguments for the splice call to apply to ourselves.
|
||
|
var newSpliceArgs = new Array(2 + splice.added);
|
||
|
newSpliceArgs[0] = splice.start;
|
||
|
newSpliceArgs[1] = numDeleted;
|
||
|
for (i = 2; i < newSpliceArgs.length; i++) {
|
||
|
newSpliceArgs[i] = null;
|
||
|
}
|
||
|
|
||
|
// Apply the splice to ourselves, inserting nulls for the newly-added items.
|
||
|
this.arraySplice(splice.start, numDeleted, gutil.arrayRepeat(splice.added, null));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns the rowId at the given index from the rowIdArray. (Subscribes if called in a computed.)
|
||
|
*/
|
||
|
LazyArrayModel.prototype.getRowId = function(index) {
|
||
|
return this._rowIdArray.at(index);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns the index of the given rowId, or -1 if not found. (Does not subscribe to array.)
|
||
|
*/
|
||
|
LazyArrayModel.prototype.getRowIndex = function(rowId) {
|
||
|
return this._rowIdArray.peek().indexOf(rowId);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns the index of the given rowId, or -1 if not found. (Subscribes if called in a computed.)
|
||
|
*/
|
||
|
LazyArrayModel.prototype.getRowIndexWithSub = function(rowId) {
|
||
|
return this._rowIdArray.all().indexOf(rowId);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns the rowModel for the given rowId.
|
||
|
* Returns undefined when there is no rowModel for the given rowId, which is often the case
|
||
|
* when it is scrolled out of view.
|
||
|
*/
|
||
|
LazyArrayModel.prototype.getRowModel = function(rowId) {
|
||
|
return this._assignedRowModels.get(rowId);
|
||
|
};
|
||
|
|
||
|
module.exports = DataTableModel;
|