UNPKG

tui-grid

Version:

TOAST UI Grid : Powerful data grid control supported by TOAST UI

657 lines (562 loc) 22.8 kB
/** * @fileoverview Grid 의 Data Source 에 해당하는 Model 정의 * @author NHN. FE Development Lab <dl_javascript@nhn.com> */ 'use strict'; var _ = require('underscore'); var Backbone = require('backbone'); var snippet = require('tui-code-snippet'); var Model = require('../../base/model'); var ExtraDataManager = require('./extraDataManager'); var GridEvent = require('../../event/gridEvent'); var util = require('../../common/util'); var clipboardUtil = require('../../common/clipboardUtil'); var classNameConst = require('../../common/classNameConst'); // Propertie names that indicate meta data var PRIVATE_PROPERTIES = [ '_button', '_number', '_extraData' ]; // Error code for validtaion var VALID_ERR_REQUIRED = 'REQUIRED'; var VALID_ERR_TYPE_NUMBER = 'TYPE_NUMBER'; /** * Data 중 각 행의 데이터 모델 (DataSource) * @module model/data/row * @extends module:base/model * @ignore */ var Row = Model.extend(/** @lends module:model/data/row.prototype */{ initialize: function() { Model.prototype.initialize.apply(this, arguments); this.extraDataManager = new ExtraDataManager(this.get('_extraData')); this.columnModel = this.collection.columnModel; this.validateMap = {}; this.on('change', this._onChange, this); }, idAttribute: 'rowKey', /** * Overrides Backbone's set method for executing onBeforeChange before firing change event. * @override * @param {(Object|string)} key - Model's attribute(s) * @param {*} value - Model's value or options when type of key paramater is object * @param {?Object} options - The value of key or the options object */ set: function(key, value, options) { var isObject = _.isObject(key); var changedColumns; // When the "key" parameter's type is object, // the "options" parameter is replaced by the "value" parameter. if (isObject) { options = value; } // When calling set method on initialize, the value of columnModel is undefined. if (this.columnModel && !(options && options.silent)) { if (isObject) { changedColumns = key; } else { changedColumns = {}; changedColumns[key] = value; } _.each(changedColumns, function(columnValue, columnName) { if (!this._executeOnBeforeChange(columnName, columnValue)) { delete changedColumns[columnName]; } }, this); Backbone.Model.prototype.set.call(this, changedColumns, options); } else { Backbone.Model.prototype.set.apply(this, arguments); } }, /** * Overrides Backbone's parse method for extraData not to be null. * @override * @param {Object} data - initial data * @returns {Object} - parsed data */ parse: function(data) { if (!data._extraData) { data._extraData = {}; } return data; }, /** * Event handler for change event in _extraData. * Reset _extraData value with cloned object to trigger 'change:_extraData' event. * @private */ _triggerExtraDataChangeEvent: function() { this.trigger('extraDataChanged', this.get('_extraData')); }, /** * Event handler for change event in _button (=checkbox) * @param {boolean} checked - Checked state * @private */ _triggerCheckboxChangeEvent: function(checked) { var eventObj = { rowKey: this.get('rowKey') }; if (checked) { /** * Occurs when a checkbox in row header is checked * @event Grid#check * @type {module:event/gridEvent} * @property {number} rowKey - rowKey of the checked row * @property {Grid} instance - Current grid instance */ this.trigger('check', eventObj); } else { /** * Occurs when a checkbox in row header is unchecked * @event Grid#uncheck * @type {module:event/gridEvent} * @property {number} rowKey - rowKey of the unchecked row * @property {Grid} instance - Current grid instance */ this.trigger('uncheck', eventObj); } }, /** * Event handler for 'change' event. * Executes callback functions, sync rowspan data, and validate data. * @private */ _onChange: function() { var publicChanged = _.omit(this.changed, this.getPrivateProperties()); if (_.has(this.changed, '_button')) { this._triggerCheckboxChangeEvent(this.changed._button); } if (this.isDuplicatedPublicChanged(publicChanged)) { return; } _.each(publicChanged, function(value, columnName) { var columnModel = this.columnModel.getColumnModel(columnName); if (!columnModel) { return; } this.collection.syncRowSpannedData(this, columnName, value); this._executeOnAfterChange(columnName); this.validateCell(columnName, true); }, this); }, /** * Validate the cell data of given columnName and returns the error code. * @param {Object} columnName - Column name * @returns {String} Error code * @private */ _validateCellData: function(columnName) { var validation = this.columnModel.getColumnModel(columnName).validation; var errorCode = ''; var value; if (validation) { value = this.get(columnName); if (validation.required && util.isBlank(value)) { errorCode = VALID_ERR_REQUIRED; } else if (validation.dataType === 'number' && !_.isNumber(value)) { errorCode = VALID_ERR_TYPE_NUMBER; } } return errorCode; }, /** * Validate a cell of given columnName. * If the data is invalid, add 'invalid' class name to the cell. * @param {String} columnName - Target column name * @param {Boolean} isDataChanged - True if data is changed (called by onChange handler) * @returns {String} - Error code */ validateCell: function(columnName, isDataChanged) { var errorCode; if (!isDataChanged && (columnName in this.validateMap)) { return this.validateMap[columnName]; } errorCode = this._validateCellData(columnName); if (errorCode) { this.addCellClassName(columnName, classNameConst.CELL_INVALID); } else { this.removeCellClassName(columnName, classNameConst.CELL_INVALID); } this.validateMap[columnName] = errorCode; return errorCode; }, /** * Create the GridEvent object when executing changeCallback defined on columnModel * @param {String} columnName - Column name * @param {?String} columnValue - Column value * @returns {GridEvent} Event object to be passed to changeCallback * @private */ _createChangeCallbackEvent: function(columnName, columnValue) { return new GridEvent(null, { rowKey: this.get('rowKey'), columnName: columnName, value: columnValue, instance: this.collection.publicObject }); }, /** * Executes the onChangeBefore callback function. * @param {String} columnName - Column name * @param {String} columnValue - Column value * @returns {boolean} * @private */ _executeOnBeforeChange: function(columnName, columnValue) { var columnModel = this.columnModel.getColumnModel(columnName); var changed = (this.get(columnName) !== columnValue); var gridEvent; if (changed && columnModel && columnModel.onBeforeChange) { gridEvent = this._createChangeCallbackEvent(columnName, columnValue); columnModel.onBeforeChange(gridEvent); return !gridEvent.isStopped(); } return true; }, /** * Execuetes the onAfterChange callback function. * @param {String} columnName - Column name * @returns {boolean} * @private */ _executeOnAfterChange: function(columnName) { var columnModel = this.columnModel.getColumnModel(columnName); var columnValue = this.get(columnName); var gridEvent; if (columnModel.onAfterChange) { gridEvent = this._createChangeCallbackEvent(columnName, columnValue); columnModel.onAfterChange(gridEvent); return !gridEvent.isStopped(); } return true; }, /** * Returns the Array of private property names * @returns {array} An array of private property names */ getPrivateProperties: function() { return PRIVATE_PROPERTIES; }, /** * Returns the object that contains rowState info. * @returns {{disabled: boolean, isDisabledCheck: boolean, isChecked: boolean}} rowState 정보 */ getRowState: function() { return this.extraDataManager.getRowState(); }, /* eslint-disable complexity */ /** * Returns an array of all className, related with given columnName. * @param {String} columnName - Column name * @returns {Array.<String>} - An array of classNames */ getClassNameList: function(columnName) { var columnModel = this.columnModel.getColumnModel(columnName); var isMetaColumn = util.isMetaColumn(columnName); var classNameList = this.extraDataManager.getClassNameList(columnName); var cellState = this.getCellState(columnName); if (columnModel.className) { classNameList.push(columnModel.className); } if (columnModel.ellipsis) { classNameList.push(classNameConst.CELL_ELLIPSIS); } if (columnModel.validation && columnModel.validation.required) { classNameList.push(classNameConst.CELL_REQUIRED); } if (isMetaColumn) { classNameList.push(classNameConst.CELL_ROW_HEAD); } else if (cellState.editable) { classNameList.push(classNameConst.CELL_EDITABLE); } if (cellState.disabled) { classNameList.push(classNameConst.CELL_DISABLED); } if (snippet.pick(columnModel, 'editOptions', 'useViewMode') === false) { classNameList.push(classNameConst.CELL_HAS_INPUT); } return this._makeUniqueStringArray(classNameList); }, /* eslint-enable complexity */ /** * Returns a new array, which splits all comma-separated strings in the targetList and removes duplicated item. * @param {Array} targetArray - Target array * @returns {Array} - New array */ _makeUniqueStringArray: function(targetArray) { var singleStringArray = _.uniq(targetArray.join(' ').split(' ')); return _.without(singleStringArray, ''); }, /** * Returns the state of the cell identified by a given column name. * @param {String} columnName - column name * @returns {{editable: boolean, disabled: boolean}} */ getCellState: function(columnName) { var notEditableTypeList = ['_number', 'normal'], columnModel = this.columnModel, disabled = this.collection.disabled, editable = true, editType = columnModel.getEditType(columnName), rowState, relationResult; relationResult = this.executeRelationCallbacksAll(['disabled', 'editable'])[columnName]; rowState = this.getRowState(); if (!disabled) { if (columnName === '_button') { disabled = rowState.disabledCheck; } else { disabled = rowState.disabled; } disabled = disabled || !!(relationResult && relationResult.disabled); } if (_.contains(notEditableTypeList, editType)) { editable = false; } else { editable = !(relationResult && relationResult.editable === false); } return { editable: editable, disabled: disabled }; }, /** * Returns whether the cell identified by a given column name is editable. * @param {String} columnName - column name * @returns {Boolean} */ isEditable: function(columnName) { var cellState = this.getCellState(columnName); return !cellState.disabled && cellState.editable; }, /** * Returns whether the cell identified by a given column name is disabled. * @param {String} columnName - column name * @returns {Boolean} */ isDisabled: function(columnName) { var cellState = this.getCellState(columnName); return cellState.disabled; }, /** * getRowSpanData * rowSpan 설정값을 반환한다. * @param {String} [columnName] 인자가 존재하지 않을 경우, 행 전체의 rowSpanData 를 맵 형태로 반환한다. * @returns {*|{count: number, isMainRow: boolean, mainRowKey: *}} rowSpan 설정값 */ getRowSpanData: function(columnName) { var isRowSpanEnable = this.collection.isRowSpanEnable(), rowKey = this.get('rowKey'); return this.extraDataManager.getRowSpanData(columnName, rowKey, isRowSpanEnable); }, /** * Returns the _extraData.height * @returns {number} */ getHeight: function() { return this.extraDataManager.getHeight(); }, /** * Sets the height of the row * @param {number} height - height */ setHeight: function(height) { this.extraDataManager.setHeight(height); this._triggerExtraDataChangeEvent(); }, /** * rowSpanData를 설정한다. * @param {string} columnName - 컬럼명 * @param {Object} data - rowSpan 정보를 가진 객체 */ setRowSpanData: function(columnName, data) { this.extraDataManager.setRowSpanData(columnName, data); this._triggerExtraDataChangeEvent(); }, /** * rowState 를 설정한다. * @param {string} rowState 해당 행의 상태값. 'DISABLED|DISABLED_CHECK|CHECKED' 중 하나를 설정한다. * @param {boolean} silent 내부 change 이벤트 발생 여부 */ setRowState: function(rowState, silent) { this.extraDataManager.setRowState(rowState); if (!silent) { this._triggerExtraDataChangeEvent(); } }, /** * rowKey 와 columnName 에 해당하는 Cell 에 CSS className 을 설정한다. * @param {String} columnName 컬럼 이름 * @param {String} className 지정할 디자인 클래스명 */ addCellClassName: function(columnName, className) { this.extraDataManager.addCellClassName(columnName, className); this._triggerExtraDataChangeEvent(); }, /** * rowKey에 해당하는 행 전체에 CSS className 을 설정한다. * @param {String} className 지정할 디자인 클래스명 */ addClassName: function(className) { this.extraDataManager.addClassName(className); this._triggerExtraDataChangeEvent(); }, /** * rowKey 와 columnName 에 해당하는 Cell 에 CSS className 을 제거한다. * @param {String} columnName 컬럼 이름 * @param {String} className 지정할 디자인 클래스명 */ removeCellClassName: function(columnName, className) { this.extraDataManager.removeCellClassName(columnName, className); this._triggerExtraDataChangeEvent(); }, /** * rowKey 에 해당하는 행 전체에 CSS className 을 제거한다. * @param {String} className 지정할 디자인 클래스명 */ removeClassName: function(className) { this.extraDataManager.removeClassName(className); this._triggerExtraDataChangeEvent(); }, /** * ctrl + c 로 복사 기능을 사용할 때 list 형태(select, button, checkbox)의 cell 의 경우, 해당 value 에 부합하는 text로 가공한다. * List type 의 경우 데이터 값과 editOptions.listItems 의 text 값이 다르기 때문에 * text 로 전환해서 반환할 때 처리를 하여 변환한다. * * @param {string} columnName - Column name * @param {boolean} useText - Whether returns concatenated text or values * @returns {string} Concatenated text or values of "listItems" option * @private */ _getStringOfListItems: function(columnName, useText) { var value = this.get(columnName); var columnModel = this.columnModel.getColumnModel(columnName); var resultListItems, editOptionList, typeExpected, valueList, hasListItems; if (snippet.isExisty(snippet.pick(columnModel, 'editOptions', 'listItems'))) { resultListItems = this.executeRelationCallbacksAll(['listItems'])[columnName]; hasListItems = resultListItems && resultListItems.listItems; editOptionList = hasListItems ? resultListItems.listItems : columnModel.editOptions.listItems; typeExpected = typeof editOptionList[0].value; valueList = util.toString(value).split(','); if (typeExpected !== typeof valueList[0]) { valueList = _.map(valueList, function(val) { return util.convertValueType(val, typeExpected); }); } _.each(valueList, function(val, index) { var item = _.findWhere(editOptionList, {value: val}); var str = (item && (useText ? item.text : item.value)) || ''; valueList[index] = str; }, this); return valueList.join(','); } return ''; }, /** * Returns whether the given edit type is list type. * @param {String} editType - edit type * @returns {Boolean} * @private */ _isListType: function(editType) { return _.contains(['select', 'radio', 'checkbox'], editType); }, /** * change 이벤트 발생시 동일한 changed 객체의 public 프라퍼티가 동일한 경우 중복 처리를 막기 위해 사용한다. * 10ms 내에 같은 객체로 함수 호출이 일어나면 true를 반환한다. * @param {Object} publicChanged 비교할 객체 * @returns {boolean} 중복이면 true, 아니면 false */ isDuplicatedPublicChanged: function(publicChanged) { if (this._timeoutIdForChanged && _.isEqual(this._lastPublicChanged, publicChanged)) { return true; } clearTimeout(this._timeoutIdForChanged); this._timeoutIdForChanged = setTimeout(_.bind(function() { this._timeoutIdForChanged = null; }, this), 10); // eslint-disable-line no-magic-numbers this._lastPublicChanged = publicChanged; return false; }, /** * Returns the text string to be used when copying the cell value to clipboard. * @param {string} columnName - column name * @returns {string} */ getValueString: function(columnName) { var columnModel = this.columnModel; var copyText = columnModel.copyVisibleTextOfEditingColumn(columnName); var editType = columnModel.getEditType(columnName); var column = columnModel.getColumnModel(columnName); var value = this.get(columnName); if (this._isListType(editType)) { if (snippet.isExisty(snippet.pick(column, 'editOptions', 'listItems', 0, 'value'))) { value = this._getStringOfListItems(columnName, copyText); } else { throw new Error('Check "' + columnName + '"\'s editOptions.listItems property out in your ColumnModel.'); } } else if (editType === 'password') { value = ''; } value = util.toString(value); // When the value is indcluding newline text, // adding one more quotation mark and putting quotation marks on both sides. value = clipboardUtil.addDoubleQuotes(value); return value; }, /** * 컬럼모델에 정의된 relation 들을 수행한 결과를 반환한다. (기존 affectOption) * @param {Array} attrNames 반환값의 결과를 확인할 대상 callbackList. * (default : ['listItems', 'disabled', 'editable']) * @returns {{}|{columnName: {attribute: *}}} row 의 columnName 에 적용될 속성값. */ executeRelationCallbacksAll: function(attrNames) { var rowData = this.attributes; var relationsMap = this.columnModel.get('relationsMap'); var result = {}; if (_.isEmpty(attrNames)) { attrNames = ['listItems', 'disabled', 'editable']; } _.each(relationsMap, function(relations, columnName) { var value = rowData[columnName]; _.each(relations, function(relation) { this._executeRelationCallback(relation, attrNames, value, rowData, result); }, this); }, this); return result; }, /** * Executes relation callback * @param {Object} relation - relation object * @param {array} relation.targetNames - target column list * @param {function} [relation.disabled] - callback function for disabled attribute * @param {function} [relation.editable] - callback function for disabled attribute * @param {function} [relation.listItems] - callback function for changing option list * @param {array} attrNames - an array of callback names * @param {(string|number)} value - cell value * @param {Object} rowData - all value of the row * @param {Object} result - object to store the result of callback functions * @private */ _executeRelationCallback: function(relation, attrNames, value, rowData, result) { var rowState = this.getRowState(); var targetNames = relation.targetNames; _.each(attrNames, function(attrName) { var callback; if (!rowState.disabled || attrName !== 'disabled') { callback = relation[attrName]; if (typeof callback === 'function') { _.each(targetNames, function(targetName) { result[targetName] = result[targetName] || {}; result[targetName][attrName] = callback(value, rowData); }, this); } } }, this); } }, { privateProperties: PRIVATE_PROPERTIES }); module.exports = Row;