From 726364d091f2506c5d990fe45b93cd99485692c4 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Thu, 22 Jun 2006 14:01:54 +0000 Subject: [PATCH] Scholar.History -- i.e. undo/redo functionality Partially integrated into data layer, but I'm waiting to commit that part until I'm sure it won't break everything --- .../content/scholar/xpcom/history.js | 429 ++++++++++++++++++ .../content/scholar/xpcom/schema.js | 6 +- components/chnmIScholarService.js | 4 + schema.sql | 29 +- 4 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 chrome/chromeFiles/content/scholar/xpcom/history.js diff --git a/chrome/chromeFiles/content/scholar/xpcom/history.js b/chrome/chromeFiles/content/scholar/xpcom/history.js new file mode 100644 index 0000000000..1b88a9b00e --- /dev/null +++ b/chrome/chromeFiles/content/scholar/xpcom/history.js @@ -0,0 +1,429 @@ +Scholar.History = new function(){ + this.begin = begin; + this.add = add; + this.modify = modify; + this.remove = remove; + this.commit = commit; + this.cancel = cancel; + this.getPreviousEvent = getPreviousEvent; + this.getNextEvent = getNextEvent; + this.undo = undo; + this.redo = redo; + this.clear = clear; + this.clearAfter = clearAfter; + + var _firstTime = true; + var _currentID = 0; + var _activeID; + var _activeEvent; + var _maxID = 0; + + // event: ('item-add', 'item-delete', 'item-modify', 'collection-add', 'collection-modify', 'collection-delete') + // context: (itemCreators.itemID-creatorID.1-1) + // action: ('add', 'delete', 'modify') + + /** + * Begin a transaction set + **/ + function begin(event, id){ + if (_activeID){ + throw('History transaction set already in progress'); + } + + // If running for the first time this session or we're in the middle of + // the history, clear any transaction sets after the current position + if (_firstTime || _currentID<_maxID){ + _firstTime = false; + this.clearAfter(); + } + + Scholar.debug('Beginning history transaction set ' + event); + var sql = "INSERT INTO transactionSets (event, id) VALUES " + + "('" + event + "', "; + // If integer, insert natively; if array, insert as string + sql += (typeof id=='object') ? "'" + id.join('-') + "'" : id; + sql += ")"; + + Scholar.DB.beginTransaction(); + _activeID = Scholar.DB.query(sql); + _activeEvent = event; + } + + + /** + * Add an add transaction to the current set + **/ + function add(table, key, keyValues){ + return _addTransaction('add', table, key, keyValues); + } + + + /** + * Add a modify transaction to the current set + * + * _field_ is optional -- otherwise all fields are saved + **/ + function modify(table, key, keyValues, field){ + return _addTransaction('modify', table, key, keyValues, field); + } + + + /** + * Add a remove transaction to the current set + **/ + function remove(table, key, keyValues){ + return _addTransaction('remove', table, key, keyValues); + } + + + /** + * Commit the current transaction set + **/ + function commit(){ + Scholar.debug('Committing history transaction set ' + _activeEvent); + Scholar.DB.commitTransaction(); + _currentID = _activeID; + _maxID = _activeID; + _activeID = null; + _activeEvent = null; + } + + + /** + * Cancel the current transaction set + **/ + function cancel(){ + Scholar.debug('Cancelling history transaction set ' + _activeEvent); + Scholar.DB.rollbackTransaction(); + _activeID = null; + _activeEvent = null; + } + + + /** + * Get the next event to undo, or false if none + **/ + function getPreviousEvent(){ + if (!_currentID){ + return false; + } + + var sql = "SELECT event FROM transactionSets WHERE transactionSetID=" + + _currentID; + return Scholar.DB.valueQuery(sql); + } + + + /** + * Get the next event to redo, or false if none + **/ + function getNextEvent(){ + var sql = "SELECT event FROM transactionSets WHERE transactionSetID=" + + (_currentID + 1); + return Scholar.DB.valueQuery(sql); + } + + + /** + * Undo the last transaction set + **/ + function undo(){ + if (!_currentID){ + throw('No transaction set to undo'); + return false; + } + + var id = _currentID; + Scholar.debug('Undoing transaction set ' + id); + Scholar.DB.beginTransaction(); + var undone = _do('undo'); + _currentID--; + Scholar.DB.commitTransaction(); + _notifyEvent(id); + return true; + } + + + /** + * Redo the next transaction set + **/ + function redo(){ + var id = _currentID + 1; + Scholar.debug('Redoing transaction set ' + id); + Scholar.DB.beginTransaction(); + var redone = _do('redo'); + _currentID++; + Scholar.DB.commitTransaction(); + _notifyEvent(id); + return redone; + } + + + /** + * Clear the entire history + **/ + function clear(){ + Scholar.DB.beginTransaction(); + Scholar.DB.query("DELETE FROM transactionSets"); + Scholar.DB.query("DELETE FROM transactions"); + Scholar.DB.query("DELETE FROM transactionLog"); + _currentID = null; + _activeID = null; + _activeEvent = null; + _maxID = null; + Scholar.DB.commitTransaction(); + } + + + /** + * Clear all transactions in history after the current one + **/ + function clearAfter(){ + Scholar.DB.beginTransaction(); + var min = Scholar.DB.valueQuery("SELECT MIN(transactionID) FROM " + + "transactions WHERE transactionSetID=" + (_currentID + 1)); + + if (!min){ + Scholar.DB.rollbackTransaction(); + return; + } + + Scholar.DB.query("DELETE FROM transactionLog " + + "WHERE transactionID>=" + min); + Scholar.DB.query("DELETE FROM transactions " + + "WHERE transactionID>=" + min); + Scholar.DB.query("DELETE FROM transactionSets " + + "WHERE transactionSetID>" + _currentID); + + _maxID = _currentID; + _activeID = null; + Scholar.DB.commitTransaction(); + return; + } + + + // + // Private methods + // + + function _addTransaction(action, table, key, keyValues, field){ + if (!_activeID){ + throw('Cannot add history transaction with no transaction set in progress'); + } + + if (typeof keyValues == 'object'){ + keyValues = keyValues.join('-'); + } + + var contextString = table + '.' + key + '.' + keyValues; + var context = _parseContext(contextString); + var fromClause = _contextToSQLFrom(context); + + var sql = "INSERT INTO transactions (transactionSetID, context, action) " + + "VALUES (" + _activeID + ", '" + contextString + + "', '" + action + "')"; + + var transactionID = Scholar.DB.query(sql); + + switch (action){ + case 'add': + // No need to store an add, since we'll just delete it to reverse + break; + case 'modify': + // Only save one field -- _do() won't know about this, but the + // UPDATE statements on the other fields just won't do anything + if (field){ + var sql = "INSERT INTO transactionLog SELECT " + transactionID + + ", '" + field + "', " + field + fromClause; + Scholar.DB.query(sql); + break; + } + // Fall through if no field specified and save all + case 'remove': + var cols = Scholar.DB.getColumns(table); + for (var i in cols){ + // If column is not part of the key, log it + if (!Scholar.inArray(cols[i], context['keys'])){ + var sql = "INSERT INTO transactionLog " + + "SELECT " + transactionID + ", '" + cols[i] + + "', " + cols[i] + fromClause; + Scholar.DB.query(sql); + } + } + break; + default: + Scholar.DB.rollbackTransaction(); + throw("Invalid history action '" + action + "'"); + } + } + + + function _do(mode){ + switch (mode){ + case 'undo': + var id = _currentID; + break; + case 'redo': + var id = _currentID + 1; + break; + } + + var sql = "SELECT transactionID, context, action FROM transactions " + + "WHERE transactionSetID=" + id; + var transactions = Scholar.DB.query(sql); + + if (!transactions){ + throw('Transaction set not found for ' + + (mode=='undo' ? 'current' : 'next') + id); + } + + for (var i in transactions){ + var transactionID = transactions[i]['transactionID']; + var context = _parseContext(transactions[i]['context']); + + // If in redo mode, swap 'add' and 'remove' + if (mode=='redo'){ + switch (transactions[i]['action']){ + case 'add': + transactions[i]['action'] = 'remove'; + break; + case 'remove': + transactions[i]['action'] = 'add'; + break; + } + } + + switch (transactions[i]['action']){ + case 'add': + var fromClause = _contextToSQLFrom(context); + + // First, store the row we're about to delete for later redo + var cols = Scholar.DB.getColumns(context['table']); + for (var i in cols){ + // If column is not part of the key, log it + if (!Scholar.inArray(cols[i], context['keys'])){ + var sql = "INSERT INTO transactionLog " + + "SELECT " + transactionID + ", '" + cols[i] + + "', " + cols[i] + fromClause; + Scholar.DB.query(sql); + } + } + + // And delete the row + var sql = "DELETE" + fromClause; + Scholar.DB.query(sql); + break; + + case 'modify': + // Retrieve old values + var sql = "SELECT field, value FROM transactionLog " + + "WHERE transactionID=" + transactionID; + var oldFieldValues = Scholar.DB.query(sql); + + // Retrieve new values + var sql = "SELECT *" + _contextToSQLFrom(context); + var newValues = Scholar.DB.rowQuery(sql); + + // Update row with old values + var sql = "UPDATE " + context['table'] + " SET "; + var values = []; + for (var i in oldFieldValues){ + sql += oldFieldValues[i]['field'] + '=?, '; + values.push(oldFieldValues[i]['value']); + } + sql = sql.substr(0, sql.length-2) + _contextToSQLWhere(context); + Scholar.DB.query(sql, values); + + // Update log with new values for later redo + for (var i in newValues){ + if (!Scholar.inArray(i, context['keys'])){ + var sql = "UPDATE transactionLog SET " + + "value=? WHERE transactionID=? AND field=?"; + Scholar.DB.query(sql, [i, newValues[i], transactionID]); + } + } + break; + + case 'remove': + // Retrieve old values + var sql = "SELECT field, value FROM transactionLog " + + "WHERE transactionID=" + transactionID; + var oldFieldValues = Scholar.DB.query(sql); + + // Add key to parameters + var fields = [], values = [], marks = []; + for (var i=0; i