/* * file: OperationManager.js * * @BEGINLICENSE * Copyright 2010 Brook Novak (email : brooknovak@seaweed-editor.com) * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * @ENDLICENSE */ bootstrap.provides("OperationManager"); /* Read Only. Stores all undoable/redoable operation logic */ var _operationRepository = {}, /* Read Only. Stores the current list of operations. Null/undefined if there is none */ _curOperationList, /* Set to true to record operations, false to ignore. Defaults to true. */ _recordOperations = true; /** * * @param {Number} opCode The unique operation code which identifies the operation. * * @param {Function} exec The execution function. The first argument will be the operation data. * The following arguments are specific execution arguemnts to the operation. * * @param {Function} undo The undo function. Given one argument: the operation data. * * @param {Function} redo The redo function. Given one argument: the operation data. * * @return {Number} The operation code of the registered operation */ function _registerOperation(opCode, exec, undo, redo) { // Should generate operation code? if (!opCode) { opCode = _registerOperation.genOp + 1; do { opCode++; } while(_operationRepository[opCode]); _registerOperation.genOp = opCode; } debug.assert(!_operationRepository[opCode], "Attempted to override operation with op code: " + opCode); _operationRepository[opCode] = {exec:exec,undo:undo,redo:redo}; return opCode; } _registerOperation.genOp = 100; /** * Executes an undoable operation. If _recordOperations is on then the operation will be stored * in the _curOperationList list. * * When ever dom is to be manipulated, it is always done here. The additional arguments after the given op code * are specific to the operation. * * @param {Number} opCode The operation code of the operation to execute. */ function _execOp(opCode) { // Get the operation code var operation = _operationRepository[opCode]; debug.assert(operation != null, "Unknown operation: " + opCode); // Create argument list... begin with the operation data object var args = Array.prototype.slice.call(arguments); args.shift(); args.unshift({opCode:opCode}); // Execute the operation var opRes = operation.exec.apply(operation, args); // Store the operation data if (_recordOperations) { // Create a new operation list if not appending if (!_curOperationList) _curOperationList = []; _curOperationList.push(args[0]); } // Return the operatoin-specific result return opRes; } /** * Note: Wipes current operation list. * @return {[Object]} The current list of operations. Null if there are none. */ function _getOperations() { var ops = _curOperationList; _curOperationList = null; return ops; } /** * Undoes a list of operations. Assumes that the effected DOM state is the same as it was after the operations were executed. * * @param {[Object]} opList The list of operations to undo */ function _undoOperations(opList) { for (var i = opList.length - 1; i >= 0; i--) { var opData = opList[i]; var operation = _operationRepository[opData.opCode]; debug.assert(operation != null, "Unknown operation: " + opData.opCode); operation.undo(opData); } } /** * Redoes a list of operations. Assumes that the effected DOM state is the same as it was after the operations were undone. * * @param {[Object]} opList The list of operations to redo */ function _redoOperations(opList) { for (var i in opList) { var opData = opList[i]; var operation = _operationRepository[opData.opCode]; debug.assert(operation != null, "Unknown operation: " + opData.opCode); operation.redo(opData); } } /** * Controls whether undoable operations should be recorded. * ONLY USE IF YOU KNOW WHAT YOU ARE DOING. * * TODO: Detailed doc. * * @param {Boolean} on True to turn on operation recording. False to turn off. */ de.recordOperations = function(on) { _recordOperations = on; }; /* BASE OPERATIONS */ // @DEBUG ON _Operation = { // @REPLACE _Operation.INSERT_NODE 1 INSERT_NODE : 1, // @REPLACE _Operation.REMOVE_NODE 2 REMOVE_NODE : 2, // @REPLACE _Operation.SPLIT_TEXT_NODE 3 SPLIT_TEXT_NODE : 3, // @REPLACE _Operation.INSERT_TEXT 4 INSERT_TEXT : 4, // @REPLACE _Operation.REMOVE_TEXT 5 REMOVE_TEXT : 5, // @REPLACE _Operation.SET_CSS_STYLE 6 SET_CSS_STYLE : 6, // @REPLACE _Operation.SET_CLASS 7 SET_CLASS : 7, // @REPLACE _Operation.INSERT_ROW 8 INSERT_ROW : 8, // @REPLACE _Operation.INSERT_CELL 9 INSERT_CELL : 9, // @REPLACE _Operation.DELETE_ROW 10 DELETE_ROW : 10, // @REPLACE _Operation.DELETE_CELL 11 DELETE_CELL : 11 }; // @DEBUG OFF _registerOperation(_Operation.INSERT_NODE, /** * Execute * @param {Object} data The operation data * @param {Node} newNode The new dom node to insert * @param {Node} parent The parent of the dom node to insert into * @param {Number} index The index in the parent to insert the dom node. Omit to append */ function(data, newNode, parent, index){ data.newNode = newNode; data.parent = parent; if (index || index === 0) // Is it an append operation? data.pos = index; this.redo(data); }, /* Undo */ function(data){ data.parent.removeChild(data.newNode); }, /* Redo */ function(data){ if (data.pos || data.pos === 0) _insertAt(data.parent, data.newNode, data.pos); else data.parent.appendChild(data.newNode); } ); _registerOperation(_Operation.REMOVE_NODE, /** * Execute * @param {Object} data The operation data * @param {Node} target The Node to remove */ function(data, target){ data.parent = target.parentNode; data.pos = _indexInParent(target); data.target = target; this.redo(data); }, /* Undo */ function(data){ _insertAt(data.parent, data.target, data.pos); }, /* Redo */ function(data){ data.parent.removeChild(data.target); } ); _registerOperation(_Operation.SPLIT_TEXT_NODE, /* Execute */ function(data, target, index){ data.target = target; data.index = index; data.rem = target.splitText(index); return data.rem; }, /* Undo */ function(data){ // Restore target nodes full text value data.target.nodeValue += data.rem.nodeValue; // Get rid of the split text node data.rem.parentNode.removeChild(data.rem); data.rem.nodeValue = ""; // free some memory }, /* Redo */ function(data){ var fullText = data.target.nodeValue; // Re-set the splitted nodes text data.rem.nodeValue = fullText.substr(data.index); data.target.nodeValue = fullText.substr(0, data.index); // Re-insert the split node _insertAfter(data.rem, data.target); } ); _registerOperation(_Operation.INSERT_TEXT, /* Execute */ function(data, target, text, index){ data.target = target; data.index = index; data.len = text.length; var pre = target.nodeValue.substr(0, index), post = target.nodeValue.substr(index); target.nodeValue = pre + text + post; }, /* Undo */ function(data){ data.text = data.target.nodeValue.substr(data.index, data.len); var pre = data.target.nodeValue.substr(0, data.index), post = data.target.nodeValue.substr(data.index + data.len); data.target.nodeValue = pre + post delete data["len"]; // Free some memory }, /* Redo */ function(data){ data.len = data.text.length; var pre = data.target.nodeValue.substr(0, data.index), post = data.target.nodeValue.substr(data.index); data.target.nodeValue = pre + data.text + post; delete data["text"]; // Free some memory } ); _registerOperation(_Operation.REMOVE_TEXT, /* Execute */ function(data, target, index, length){ data.target = target; data.index = index; data.text = target.nodeValue.substr(index, length); var pre = target.nodeValue.substr(0, index), post = target.nodeValue.substr(index + length); target.nodeValue = pre + post }, /* Undo - same as insert text's undo */ _operationRepository[_Operation.INSERT_TEXT].redo, /* Redp - same as insert text's undo */ _operationRepository[_Operation.INSERT_TEXT].undo ); _registerOperation(_Operation.SET_CSS_STYLE, /** * Execute * @param {Object} data The operation data * @param {Node} target The target element to set CSS * @param {String} css The CSS Field to set in javascript notation * @param {String} value The value of the CSS to set */ function(data, target, css, value){ data.target = target; data.css = css; data.newValue = value; data.oldValue = _engine == _Platform.TRIDENT ? data.target.style.getAttribute(data.css) : data.target.style[data.css]; this.redo(data); }, /* Undo */ function(data){ if (_engine == _Platform.TRIDENT) data.target.style.setAttribute(data.css, data.oldValue); else data.target.style[data.css] = data.oldValue; }, /* Redo */ function(data){ if (_engine == _Platform.TRIDENT) data.target.style.setAttribute(data.css, data.newValue); else data.target.style[data.css] = data.newValue; } ); _registerOperation(_Operation.SET_CLASS, /** * Execute * @param {Object} data The operation data * @param {Node} target The target element to set CSS * @param {String} name The class name to set (replaces full class name) */ function(data, target, name){ data.target = target; data.newName = name; data.oldName = _getClassName(target); this.redo(data); }, /* Undo */ function(data){ _setClassName(data.target, data.oldName); }, /* Redo */ function(data){ _setClassName(data.target, data.newName); } ); _registerOperation(_Operation.INSERT_CELL, /** * Execute * @param {Object} data The operation data * @param {Node} row The target row element to insert the cell into * @param {Number} index The index in the row to insert into. Clamped if out of bounds * @return {Node} The new cell that was created */ function(data, row, index){ data.row = row; data.index = index > row.cells.length ? row.cells.length : index; // Safely clamp range if (data.index < 0) data.index = 0; return this.redo(data); }, /* Undo */ function(data){ data.row.deleteCell(data.index); }, /* Redo */ function(data){ return data.row.insertCell(data.index); } ); _registerOperation(_Operation.INSERT_ROW, /** * Execute * @param {Object} data The operation data * @param {Node} table The target table element to insert the row into * @param {Number} index The index in the row to insert into. Clamped if out of bounds * @return {Node} The new row that was created */ function(data, table, index){ data.table = table; data.index = index > table.rows.length ? table.rows.length : index; // Safely clamp range if (data.index < 0) data.index = 0; return this.redo(data); }, /* Undo */ function(data){ data.table.deleteRow(data.index); }, /* Redo */ function(data){ return data.table.insertRow(data.index); } ); _registerOperation(_Operation.DELETE_ROW, /** * Execute * @param {Object} data The operation data * @param {Node} table The target table element to insert the row into * @param {Number} index The index in the row to delete. Clamped if out of bounds */ function(data, table, index){ data.table = table; data.index = index >= table.rows.length ? table.rows.length-1 : index; // Safely clamp range if (data.index < 0) data.index = 0; this.redo(data); }, /* Undo */ function(data){ var newRow = data.table.insertRow(data.index); // Migrate contents from old removed row into new row while(data.row.firstChild) { var migrant = data.row.firstChild; data.row.removeChild(migrant); newRow.appendChild(migrant); } // Save memory - get rid of old removed row reference delete data["row"]; }, /* Redo */ function(data){ data.row = data.table.rows[data.index]; // Save row - need to keep contents data.table.deleteRow(data.index); } ); _registerOperation(_Operation.DELETE_CELL, /** * Execute * @param {Object} data The operation data * @param {Node} row The target row element to delete the cell from * @param {Number} index The cell index in the row to delete. Clamped if out of bounds */ function(data, row, index){ data.row = row; data.index = index >= row.cells.length ? row.cells.length-1 : index; // Safely clamp range if (data.index < 0) data.index = 0; this.redo(data); }, /* Undo */ function(data){ var newCell = data.row.insertCell(data.index); // Migrate contents from old removed row into new row while(data.cell.firstChild) { var migrant = data.cell.firstChild; data.cell.removeChild(migrant); newCell.appendChild(migrant); } // Save memory - get rid of old removed row reference delete data["cell"]; }, /* Redo */ function(data){ data.cell = data.row.cells[data.index]; // Save cell - need to keep contents data.row.deleteCell(data.index); } );