/** * @class Spread.selection.Position * * Class for representing a position in the grid view / selection model. * Instances of this class are used for identifying all components which * belong to a single cell. Such as: * <ul> * <li>The view</li> * <li>The column index</li> * <li>The row index</li> * <li>The row data record</li> * <li>The data record's model class</li> * <li>The column header</li> * <li>The row element (html)</li> * <li>The cell element (html)</li> * </ul> * * Using this unified identification instance, extending and implementing * spreadsheets is much simpler than standard Ext JS grids. * * Before using a Position instance, please call update() to ensure all * references to be valid. */ Ext.define('Spread.selection.Position', { 'requires': ['Spread.util.State'], /** * @property {Spread.selection.Range} range * Stores the reference of the range this position belongs to (Only if it does belong to any range!) */ range: null, /** * @property {Spread.grid.Panel} spreadPanel Reference to the spread grid panel */ spreadPanel: null, /** * @property {Spread.grid.View} view * View instance the position belongs to */ view: null, /** * @property {Number} column * Column index, the position belongs to */ column: -1, /** * @property {Number} row * Row index, the position belongs to */ row: -1, /** * @property {Ext.data.Model} record * Data record (instance of Ext.data.Model) the position belongs to (in store). * If not given, this will be auto-detected. */ record: null, /** * @property {Function} model * Reference to the model class constructor of the record of the position. * If not given, this will be auto-detected. */ model: null, /** * @property {Ext.grid.column.Column} columnHeader * Column instance (header) the position belongs to. * If not given, this will be auto-detected. */ columnHeader: null, /** * @property {HTMLElement} rowEl * Row element instance <tr>, the position belongs to. * If not given, this will be auto-detected. */ rowEl: null, /** * @property {HTMLElement} cellEl * Cell element instance <td>, the position belongs to. * If not given, this will be auto-detected. */ cellEl: null, /** * @property {Boolean} editable * Indicator flag describing if the position is editable */ editable: true, /** * @property {Boolean} selectable * Indicator flag describing if the position is selectable */ selectable: true, /** * @property {Boolean} editModeStyling * Indicator flag describing if the position should be specially colored when editable */ editModeStyling: true, /** * @property {Boolean} focused * Status flag if the position is currently being focused */ focused: false, /** * @property {Boolean} editing * Status flag if the position is currently in edit mode */ editing: false, /** * @property {Boolean} selected * Status flag if the position resists currently in a selected range */ selected: false, /** * Creates a position object which points to a cell position, record, column-header * and view reference. For performance reasons, try to call this function with all * arguments. More arguments given, means less auto detection effort. * @param {Spread.grid.View} view Spread view instance reference * @param {Number} columnIndex Column index * @param {Number} rowIndex Row index * @param {Ext.data.Model} [record=auto-detect] Data record instance * @param {HTMLElement} [rowEl=auto-detect] Row's HTML element (tr-element) * @param {HTMLElement} [cellEl=auto-detect] Cell's HTML element (td-element) * @return */ constructor: function(view, columnIndex, rowIndex, record, rowEl, cellEl) { // Correct row and column index if outside of possible grid boundings var maxRowCount = view.getStore().getCount(); if (Ext.versions.extjs.major === 4 && Ext.versions.extjs.minor < 2) { var maxColumnCount = view.headerCt.getGridColumns(true).length; } else { var maxColumnCount = view.getGridColumns().length; } // Column boundary protection if (columnIndex >= maxColumnCount) { columnIndex = (maxColumnCount-1); } // Row boundary protection if (rowIndex >= maxRowCount) { rowIndex = (maxRowCount-1); } var rowEl = rowEl || view.getNode(rowIndex), record = record || view.getStore().getAt(rowIndex), model = null; // Try to auto-detect if (rowEl) { cellEl = cellEl || rowEl.childNodes[columnIndex]; } else { cellEl = cellEl || null; } // If record was given or detected, map it's model reference if (record) { model = record.self; } // State data: editable var editableState = Spread.util.State.getPositionState(this, 'editable'); if (Ext.isDefined(editableState)) { this.editable = editableState; } else { Spread.util.State.setPositionState(this, 'editable', this.editable); } // State data: editmodestyling var editModeStylingState = Spread.util.State.getPositionState(this, 'editmodestyling'); if (Ext.isDefined(editModeStylingState)) { this.editModeStyling = editModeStylingState; } else { Spread.util.State.setPositionState(this, 'editmodestyling', this.editModeStyling); } // State data: selectable var selectableState = Spread.util.State.getPositionState(this, 'selectable'); if (Ext.isDefined(selectableState)) { this.selectable = selectableState; } else { Spread.util.State.setPositionState(this, 'selectable', this.selectable); } //console.log('TRY FETCH ROW td', rowEl); //console.log('TRY FETCH CELL td', cellEl); Ext.apply(this, { view: view, spreadPanel: view.getSpreadPanel(), column: columnIndex, row: rowIndex, record: model, model: record ? record.self : undefined, columnHeader: view.getHeaderAtIndex(columnIndex), rowEl: rowEl, cellEl: cellEl }); }, /** * (Re)validates the position object and it's internal references. * This is useful when view has been refreshed and record or * cell or row of the position has been changed. * @return {Spread.selection.Position} */ validate: function() { this.spreadPanel = this.view.getSpreadPanel(); this.record = this.view.getStore().getAt(this.row); // If record was given or detected, map it's model reference if (this.record) { this.model = this.record.self; } else { this.model = null; } // Assign updated values/references this.columnHeader = this.view.getHeaderAtIndex(this.column); this.rowEl = this.view.getNode(this.row); if (this.rowEl) { this.cellEl = this.rowEl.childNodes[this.column]; } // State data: editable var editableState = Spread.util.State.getPositionState(this, 'editable'); if (Ext.isDefined(editableState)) { this.editable = editableState; } // State data: editmodestyling var editModeStylingState = Spread.util.State.getPositionState(this, 'editmodestyling'); if (Ext.isDefined(editModeStylingState)) { this.editModeStyling = editModeStylingState; } // State data: selectable var selectableState = Spread.util.State.getPositionState(this, 'selectable'); if (Ext.isDefined(selectableState)) { this.selectable = selectableState; } //console.log('Position update()ed ', this); return this; }, /** * Get field name by fetching the dataIndex * of a given column index from column header container of the view. * @return {String} */ getFieldName: function() { var me = this, header = me.view.getHeaderAtIndex(me.column); if (header) { return header.dataIndex; } else { throw "No column found for column index: " + me.column; } }, /** * Returns the primitive type of the position. * Returns either: auto, int, float, bool, string or date. * @return {String} */ getType: function() { var me = this, type = 'auto', fieldName = me.getFieldName(); // Caching type name on record instance if (!me.record['__' + fieldName + '_type']) { me.record.fields.each(function(field) { // Found the field and it's special type if (field.name === fieldName && field.type.type !== 'auto') { type = field.type.type; } }); return me.record['__' + fieldName + '_type'] = type; } return me.record['__' + fieldName + '_type']; }, /** * Casts a new value into the primitive type * needed by the position's under-laying model. * @param {String} newValue New value * @return {*} */ castNewValue: function(newValue) { var me = this; // null or undefined if (!Ext.isDefined(newValue) || newValue === null) { return newValue; } // Do casting switch(me.getType()) { case 'bool': return (newValue == 'true'); case 'int': return parseInt(newValue); case 'float': return parseFloat(newValue); case 'auto': case 'string': return newValue.valueOf(); case 'date': return new Date(newValue); } }, /** * Returns, what Ext.data.Model's set() returns. * (An array of modified field names or null if nothing was modified) * * @param {Mixed} newValue New cell value * @param {Boolean} [autoCommit=false] Should the record be automatically committed after change * @param {Boolean} [useInternalAPIs=false] Force to use the internal Model API's * @return {String} */ setValue: function(newValue, autoCommit, useInternalAPIs) { var me = this, fieldName, ret; // Update position me.validate(); if (!autoCommit && !me.columnHeader.autoCommit) { //useInternalAPIs = true; } // Fetch field name fieldName = me.getFieldName(); if (!me.record) { throw "No record found for row index: " + me.row; } // Check for pre-processor if (me.columnHeader.cellwriter && Ext.isFunction(me.columnHeader.cellwriter)) { // Call pre-processor for value writing / change before write newValue = me.columnHeader.cellwriter(newValue, me); } else { // Auto-preprocessor (type conversion) // Casting the new value from text received from // the text input field into the origin data type newValue = me.castNewValue(newValue); } // Do not change the record's value if it hasn't changed if (me.record.get(fieldName) == newValue) { return newValue; } if (useInternalAPIs) { // Set specific modified field value me.record.modified[fieldName] = newValue; // Set record dirty me.record.dirty = true; // Set specific field's value ret = me.record[me.record.persistenceProperty][fieldName] = newValue; } else { // Set new value ret = me.record.set(fieldName, newValue); } // Automatically commit if wanted if (autoCommit && me.columnHeader.autoCommit) { me.record.commit(); } return ret; }, /** * Returns the value of the position * @return {*} */ getValue: function() { var me = this, fieldName, value; // Update position me.validate(); // Fetch field name fieldName = me.getFieldName(); if (!me.record) { throw "No record found for row index: " + me.row; } // Fetch raw value value = me.record.get(fieldName); // Check for pre-processor if (me.columnHeader.cellreader && Ext.isFunction(me.columnHeader.cellreader)) { // Call pre-processor for value reading value = me.columnHeader.cellreader(value, me); } return value; }, /** * Focuses the position * @return {Spread.selection.Position} */ focus: function() { if (!this.isFocusable()) return this; // Set state flag this.focused = true; // Render selected this.view.getSelectionModel().setCurrentFocusPosition(this); return this; }, /** * Returns true if the position is currently focused * @return {Boolean} */ isFocused: function() { return this.focused; }, /** * Returns true if the position is focusable * @return {Boolean} */ isFocusable: function() { return this.isSelectable(); }, /** * En/disable the position to be editable * @param {Boolean} editable Should the position be editable? * @param {Boolean} [suppressNotify=false] Used for transactions when method gets called many times, only the last call should notify the view to trigger a re-rendering. * @return {Spread.selection.Position} */ setEditable: function(editable, suppressNotify) { this.editable = editable; // Store in state manager for multi-instance flag broadcasting Spread.util.State.setPositionState(this, 'editable', editable); return this; }, /** * Returns if the position is editable. * Also checks if the column the position resides in is editable. * @return {Boolean} */ isEditable: function() { var editableState = Spread.util.State.getPositionState(this, 'editable'); if (Ext.isDefined(editableState)) { this.editable = editableState; } if (this.getColumn().editable && this.editable) { return true; } return false; }, /** * Sets the position editing * @param {Boolean} editing Should the position be editable? * @return {Spread.selection.Position} */ setEditing: function(editing) { if (!this.isEditable()) return this; this.editing = editing; if (editing) { // TODO: Activate editor //console.log('unimplemented'); } else { // TODO: Deactivate editor (inform plugin) //console.log('unimplemented'); } return this; }, /** * Returns if the position is currently being edited * @return {Boolean} */ isEditing: function() { return this.editing; }, /** * En/disable the position to be selectable * @param {Boolean} selectable Should the position be editable? * @param {Boolean} [suppressNotify=false] Used for transactions when method gets called many times, only the last call should notify the view to trigger a re-rendering. * @return {Spread.selection.Position} */ setSelectable: function(selectable, suppressNotify) { this.selectable = selectable; // Store in state manager for multi-instance flag broadcasting Spread.util.State.setPositionState(this, 'selectable', selectable); //console.log('setSelectable', this.row, this.column, selectable); return this; }, /** * Returns if the position is selectable. * Also checks if the column the position resides in is selectable or not. * @return {Boolean} */ isSelectable: function() { var selectableState = Spread.util.State.getPositionState(this, 'selectable'); if (Ext.isDefined(selectableState)) { this.selectable = selectableState; } if (this.getColumn().selectable && this.selectable) { return true; } return false; }, /** * En/disable the position to be styled specially when editable * @param {Boolean} editModeStyling Should the position be styled specially when editable? * @param {Boolean} [suppressNotify=false] Used for transactions when method gets called many times, only the last call should notify the view to trigger a re-rendering. * @return {Spread.selection.Position} */ setEditModeStyling: function(editModeStyling, suppressNotify) { this.editModeStyling = editModeStyling; // Store in state manager for multi-instance flag broadcasting Spread.util.State.setPositionState(this, 'editmodestyling', editModeStyling); return this; }, /** * Returns if the position is has edit mode styling enabled * @return {Boolean} */ hasEditModeStyling: function() { var editModeStylingState = Spread.util.State.getPositionState(this, 'editmodestyling'); if (Ext.isDefined(editModeStylingState)) { this.editModeStyling = editModeStylingState; } return this.editModeStyling; }, /** * @protected * Sets the internal selected status flag. * This method does not visually select a position. * Use the range.select() and range.deselect() methods or the * Commander API if you want to select positions (ranges!). * @param {Boolean} selected Indicates if the position should be set selected * @return {Spread.selection.Position} */ setSelected: function(selected) { if (!this.isSelectable()) return this; this.selected = selected; return this; }, /** * Returns if the position currently resists in a range currently being selected * @return {Boolean} */ isSelected: function() { return this.selected; }, /** * Sets the range instance reference * @param {Spread.selection.Range} range Selection range reference * @return {Spread.selection.Position} */ setRange: function(range) { this.range = range; return this; }, /** * Returns a range reference if given * @return {Spread.selection.Range/null} */ getRange: function() { return this.range; }, /** * Returns the selection model reference * @return {Ext.selection.Model} */ getSelectionModel: function() { return this.getSpreadPanel().getSelectionModel(); }, /** * Returns the view reference * @return {Spread.grid.View} */ getView: function() { return this.view; }, /** * Returns the spread panel reference * @return {Spread.grid.Panel} */ getSpreadPanel: function() { return this.spreadPanel; }, /** * Returns the row index * @return {Number} */ getRowIndex: function() { return this.row; }, /** * Returns the column index * @return {Number} */ getColumnIndex: function() { return this.column; }, /** * Returns the column header instance * @return {Ext.grid.column.Column} */ getColumn: function() { return this.columnHeader; }, /** * Returns the grid data record * @return {Ext.data.Record} */ getRowRecord: function() { return this.record; }, /** * Returns the model class constructor function the record is an instance of * @return {Function} */ getModelClass: function() { return this.model; } });