UNPKG

tui-grid

Version:

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

603 lines (514 loc) 19.9 kB
/** * @fileoverview Header View * @author NHN. FE Development Lab <dl_javascript@nhn.com> */ 'use strict'; var $ = require('jquery'); var _ = require('underscore'); var View = require('../../base/view'); var util = require('../../common/util'); var constMap = require('../../common/constMap'); var classNameConst = require('../../common/classNameConst'); var GridEvent = require('../../event/gridEvent'); var DragEventEmitter = require('../../event/dragEventEmitter'); var frameConst = constMap.frame; var DELAY_SYNC_CHECK = 10; var keyCodeMap = constMap.keyCode; var ATTR_COLUMN_NAME = constMap.attrName.COLUMN_NAME; var CELL_BORDER_WIDTH = constMap.dimension.CELL_BORDER_WIDTH; var TABLE_BORDER_WIDTH = constMap.dimension.TABLE_BORDER_WIDTH; // Minimum time (ms) to detect if an alert or confirm dialog has been displayed. var MIN_INTERVAL_FOR_PAUSED = 200; var Header; /** * Get count of same columns in complex columns * @param {array} currentColumn - Current column's model * @param {array} prevColumn - Previous column's model * @returns {number} Count of same columns * @ignore */ function getSameColumnCount(currentColumn, prevColumn) { var index = 0; var len = Math.min(currentColumn.length, prevColumn.length); for (; index < len; index += 1) { if (currentColumn[index].name !== prevColumn[index].name) { break; } } return index; } /** * Header Layout View * @module view/layout/header * @extends module:base/view * @param {Object} options - options * @param {String} [options.whichSide=R] R: Right, L: Left * @ignore */ Header = View.extend(/** @lends module:view/layout/header.prototype */{ initialize: function(options) { View.prototype.initialize.call(this); _.assign(this, { renderModel: options.renderModel, coordColumnModel: options.coordColumnModel, selectionModel: options.selectionModel, focusModel: options.focusModel, columnModel: options.columnModel, dataModel: options.dataModel, coordRowModel: options.coordRowModel, dimensionModel: options.dimensionModel, viewFactory: options.viewFactory, domEventBus: options.domEventBus, whichSide: options.whichSide || frameConst.R }); this.dragEmitter = new DragEventEmitter({ type: 'header', domEventBus: this.domEventBus, onDragMove: _.bind(this._onDragMove, this) }); this.listenTo(this.renderModel, 'change:scrollLeft', this._onScrollLeftChange) .listenTo(this.coordColumnModel, 'columnWidthChanged', this._onColumnWidthChanged) .listenTo(this.selectionModel, 'change:range', this._refreshSelectedHeaders) .listenTo(this.focusModel, 'change:columnName', this._refreshSelectedHeaders) .listenTo(this.dataModel, 'sortChanged', this._updateBtnSortState) .listenTo(this.dimensionModel, 'change:headerHeight', this.render); if (this.whichSide === frameConst.L && this.columnModel.get('selectType') === 'checkbox') { this.listenTo(this.dataModel, 'change:_button disabledChanged extraDataChanged add remove reset', _.debounce(_.bind(this._syncCheckedState, this), DELAY_SYNC_CHECK)); } }, className: classNameConst.HEAD_AREA, events: { 'click': '_onClick', 'keydown input': '_onKeydown', 'mousedown th': '_onMouseDown' }, /** * template */ template: _.template( '<table class="' + classNameConst.TABLE + '">' + '<colgroup><%=colGroup%></colgroup>' + '<tbody><%=tBody%></tbody>' + '</table>' ), /** * template for <th> */ templateHeader: _.template( '<th <%=attrColumnName%>="<%=columnName%>" ' + 'class="<%=className%>" ' + 'height="<%=height%>" ' + '<%if(colspan > 0) {%>' + 'colspan=<%=colspan%> ' + '<%}%>' + '<%if(rowspan > 0) {%>' + 'rowspan=<%=rowspan%> ' + '<%}%>' + '>' + '<%=title%><%=btnSort%>' + '</th>' ), /** * templse for <col> */ templateCol: _.template( '<col ' + '<%=attrColumnName%>="<%=columnName%>" ' + 'style="width:<%=width%>px">' ), /** * HTML string for a button */ markupBtnSort: '<a class="' + classNameConst.BTN_SORT + '"></a>', /** * col group 마크업을 생성한다. * @returns {string} <colgroup>에 들어갈 html 마크업 스트링 * @private */ _getColGroupMarkup: function() { var columnData = this._getColumnData(); var columnWidths = columnData.widths; var columns = columnData.columns; var htmlList = []; _.each(columnWidths, function(width, index) { htmlList.push(this.templateCol({ attrColumnName: ATTR_COLUMN_NAME, columnName: columns[index].name, width: width + CELL_BORDER_WIDTH })); }, this); return htmlList.join(''); }, /** * Returns an array of names of columns in selection range. * @private * @returns {Array.<String>} */ _getSelectedColumnNames: function() { var columnRange = this.selectionModel.get('range').column; var visibleColumns = this.columnModel.getVisibleColumns(); var selectedColumns = visibleColumns.slice(columnRange[0], columnRange[1] + 1); return _.pluck(selectedColumns, 'name'); }, _onDragMove: function(gridEvent) { var $target = $(gridEvent.target); gridEvent.setData({ columnName: $target.closest('th').attr(ATTR_COLUMN_NAME), isOnHeaderArea: $.contains(this.el, $target[0]) }); }, /** * Returns an array of names of merged-column which contains every column name in the given array. * @param {Array.<String>} columnNames - an array of column names to test * @returns {Array.<String>} * @private */ _getContainingMergedColumnNames: function(columnNames) { var columnModel = this.columnModel; var mergedColumnNames = _.pluck(columnModel.get('complexHeaderColumns'), 'name'); return _.filter(mergedColumnNames, function(mergedColumnName) { var unitColumnNames = columnModel.getUnitColumnNamesIfMerged(mergedColumnName); return _.every(unitColumnNames, function(name) { return _.contains(columnNames, name); }); }); }, /** * Refreshes selected class of every header element (th) * @private */ _refreshSelectedHeaders: function() { var $ths = this.$el.find('th'); var columnNames, mergedColumnNames; if (this.selectionModel.hasSelection()) { columnNames = this._getSelectedColumnNames(); } else if (this.focusModel.has(true)) { columnNames = [this.focusModel.get('columnName')]; } $ths.removeClass(classNameConst.CELL_SELECTED); if (columnNames) { mergedColumnNames = this._getContainingMergedColumnNames(columnNames); _.each(columnNames.concat(mergedColumnNames), function(columnName) { $ths.filter('[' + ATTR_COLUMN_NAME + '="' + columnName + '"]').addClass(classNameConst.CELL_SELECTED); }); } }, /** * Event handler for 'keydown' event on checkbox input * @param {KeyboardEvent} event - event * @private */ _onKeydown: function(event) { if (event.keyCode === keyCodeMap.TAB) { event.preventDefault(); this.focusModel.focusClipboard(); } }, /** * Mousedown event handler * @param {jQuery.Event} ev - MouseDown event * @private */ _onMouseDown: function(ev) { var $target = $(ev.target); var columnName; if (!this._triggerPublicMousedown(ev)) { return; } if ($target.hasClass(classNameConst.BTN_SORT)) { return; } columnName = $target.closest('th').attr(ATTR_COLUMN_NAME); if (columnName) { this.dragEmitter.start(ev, { columnName: columnName }); } }, /** * Trigger mousedown:body event on domEventBus and returns the result * @param {MouseEvent} ev - MouseEvent * @returns {module:event/gridEvent} * @private */ _triggerPublicMousedown: function(ev) { var startTime, endTime; var gridEvent = new GridEvent(ev, GridEvent.getTargetInfo($(ev.target))); var paused; startTime = (new Date()).getTime(); this.domEventBus.trigger('mousedown', gridEvent); endTime = (new Date()).getTime(); // check if the model window (alert or confirm) was popped up paused = (endTime - startTime) > MIN_INTERVAL_FOR_PAUSED; return !gridEvent.isStopped() && !paused; }, /** * selectType 이 checkbox 일 때 랜더링 되는 header checkbox 엘리먼트를 반환한다. * @returns {jQuery} _butoon 컬럼 헤더의 checkbox input 엘리먼트 * @private */ _getHeaderMainCheckbox: function() { return this.$el.find('th[' + ATTR_COLUMN_NAME + '="_button"] input'); }, /** * header 영역의 input 상태를 실제 checked 된 count 에 맞추어 반영한다. * @private */ _syncCheckedState: function() { var checkedState = this.dataModel.getCheckedState(); var $input, props; $input = this._getHeaderMainCheckbox(); if (!$input.length) { return; } if (!checkedState.available) { props = { checked: false, disabled: true }; } else { props = { checked: checkedState.available === checkedState.checked, disabled: false }; } $input.prop(props); }, /** * column width 변경시 col 엘리먼트들을 조작하기 위한 헨들러 * @private */ _onColumnWidthChanged: function() { var columnWidths = this.coordColumnModel.getWidths(this.whichSide); var $colList = this.$el.find('col'); var coordRowModel = this.coordRowModel; _.each(columnWidths, function(columnWidth, index) { $colList.eq(index).css('width', columnWidth + CELL_BORDER_WIDTH); }); // Calls syncWithDom only from the Rside to prevent calling twice. // Defered call to ensure that the execution occurs after both sides are rendered. if (this.whichSide === frameConst.R) { _.defer(function() { coordRowModel.syncWithDom(); }); } }, /** * scroll left 값이 변경되었을 때 header 싱크를 맞추는 이벤트 핸들러 * @param {Object} model 변경이 발생한 model 인스턴스 * @param {Number} value scrollLeft 값 * @private */ /* istanbul ignore next: scrollLeft 를 확인할 수 없음 */ _onScrollLeftChange: function(model, value) { if (this.whichSide === frameConst.R) { this.el.scrollLeft = value; } }, /** * Event handler for click event * @param {jQuery.Event} ev - MouseEvent * @private */ _onClick: function(ev) { var $target = $(ev.target); var columnName = $target.closest('th').attr(ATTR_COLUMN_NAME); var eventData = new GridEvent(ev); if (columnName === '_button' && $target.is('input')) { eventData.setData({ checked: $target.prop('checked') }); this.domEventBus.trigger('click:headerCheck', eventData); } else if ($target.is('a.' + classNameConst.BTN_SORT)) { eventData.setData({ columnName: columnName }); this.domEventBus.trigger('click:headerSort', eventData); } }, /** * 정렬 버튼의 상태를 변경한다. * @private * @param {object} sortOptions 정렬 옵션 * @param {string} sortOptions.columnName 정렬할 컬럼명 * @param {boolean} sortOptions.ascending 오름차순 여부 */ _updateBtnSortState: function(sortOptions) { var className; if (this._$currentSortBtn) { this._$currentSortBtn.removeClass(classNameConst.BTN_SORT_DOWN + ' ' + classNameConst.BTN_SORT_UP); } this._$currentSortBtn = this.$el.find( 'th[' + ATTR_COLUMN_NAME + '="' + sortOptions.columnName + '"] a.' + classNameConst.BTN_SORT ); className = sortOptions.ascending ? classNameConst.BTN_SORT_UP : classNameConst.BTN_SORT_DOWN; this._$currentSortBtn.addClass(className); }, /** * 랜더링 * @returns {View.Layout.Header} this */ render: function() { var resizeHandleHeights; this._destroyChildren(); this.$el.css({ height: this.dimensionModel.get('headerHeight') - TABLE_BORDER_WIDTH }).html(this.template({ colGroup: this._getColGroupMarkup(), tBody: this._getTableBodyMarkup() })); if (this.coordColumnModel.get('resizable')) { resizeHandleHeights = this._getResizeHandleHeights(); this._addChildren(this.viewFactory.createHeaderResizeHandle(this.whichSide, resizeHandleHeights)); this.$el.append(this._renderChildren()); } return this; }, /** * 컬럼 정보를 반환한다. * @returns {{widths: (Array|*), columns: (Array|*)}} columnWidths 와 columns 를 함께 반환한다. * @private */ _getColumnData: function() { var columnWidths = this.coordColumnModel.getWidths(this.whichSide); var columns = this.columnModel.getVisibleColumns(this.whichSide, true); return { widths: columnWidths, columns: columns }; }, /* eslint-disable complexity */ /** * Header 의 body markup 을 생성한다. * @returns {string} header 의 테이블 body 영역에 들어갈 html 마크업 스트링 * @private */ _getTableBodyMarkup: function() { var hierarchyList = this._getColumnHierarchyList(); var maxRowCount = this._getHierarchyMaxRowCount(hierarchyList); var headerHeight = this.dimensionModel.get('headerHeight'); var rowMarkupList = new Array(maxRowCount); var columnNames = new Array(maxRowCount); var colSpanList = []; var rowHeight = util.getRowHeight(maxRowCount, headerHeight) - 1; var rowSpan = 1; var height; var headerMarkupList; _.each(hierarchyList, function(hierarchy, i) { var length = hierarchyList[i].length; var curHeight = 0; _.each(hierarchy, function(columnModel, j) { var columnName = columnModel.name; var classNames = [ classNameConst.CELL, classNameConst.CELL_HEAD ]; if (columnModel.validation && columnModel.validation.required) { classNames.push(classNameConst.CELL_REQRUIRED); } rowSpan = (length - 1 === j && (maxRowCount - length + 1) > 1) ? (maxRowCount - length + 1) : 1; height = rowHeight * rowSpan; if (j === length - 1) { height = (headerHeight - curHeight) - 2; } else { curHeight += height + 1; } if (columnNames[j] === columnName) { rowMarkupList[j].pop(); colSpanList[j] += 1; } else { colSpanList[j] = 1; } columnNames[j] = columnName; rowMarkupList[j] = rowMarkupList[j] || []; rowMarkupList[j].push(this.templateHeader({ attrColumnName: ATTR_COLUMN_NAME, columnName: columnName, className: classNames.join(' '), height: height, colspan: colSpanList[j], rowspan: rowSpan, title: columnModel.title, btnSort: columnModel.sortable ? this.markupBtnSort : '' })); }, this); }, this); headerMarkupList = _.map(rowMarkupList, function(rowMarkup) { return '<tr>' + rowMarkup.join('') + '</tr>'; }); return headerMarkupList.join(''); }, /* eslint-enable complexity */ /** * column merge 가 설정되어 있을 때 헤더의 max row count 를 가져온다. * @param {Array} hierarchyList 헤더 마크업 생성시 사용될 계층구조 데이터 * @returns {number} 헤더 영역의 row 최대값 * @private */ _getHierarchyMaxRowCount: function(hierarchyList) { var lengthList = [0]; _.each(hierarchyList, function(hierarchy) { lengthList.push(hierarchy.length); }, this); return Math.max.apply(Math, lengthList); }, /** * column merge 가 설정되어 있을 때 헤더의 계층구조 리스트를 가져온다. * @returns {Array} 계층구조 리스트 * @private */ _getColumnHierarchyList: function() { var columns = this._getColumnData().columns; var hierarchyList; hierarchyList = _.map(columns, function(column) { return this._getColumnHierarchy(column).reverse(); }, this); return hierarchyList; }, /** * complexHeaderColumns 가 설정되어 있을 때 재귀적으로 돌면서 계층구조를 형성한다. * @param {Object} column - column * @param {Array} [results] - 결과로 메모이제이션을 이용하기 위한 인자값 * @returns {Array} * @private */ _getColumnHierarchy: function(column, results) { var complexHeaderColumns = this.columnModel.get('complexHeaderColumns'); results = results || []; if (column) { results.push(column); if (complexHeaderColumns) { _.each(complexHeaderColumns, function(headerColumn) { if ($.inArray(column.name, headerColumn.childNames) !== -1) { this._getColumnHierarchy(headerColumn, results); } }, this); } } return results; }, /** * Get height values of resize handlers * @returns {array} Height values of resize handles */ _getResizeHandleHeights: function() { var hierarchyList = this._getColumnHierarchyList(); var maxRowCount = this._getHierarchyMaxRowCount(hierarchyList); var rowHeight = util.getRowHeight(maxRowCount, this.headerHeight) - 1; var handleHeights = []; var index = 1; var coulmnLen = hierarchyList.length; var sameColumnCount, handleHeight; for (; index < coulmnLen; index += 1) { sameColumnCount = getSameColumnCount(hierarchyList[index], hierarchyList[index - 1]); handleHeight = rowHeight * (maxRowCount - sameColumnCount); handleHeights.push(handleHeight); } handleHeights.push(rowHeight * maxRowCount); // last resize handle return handleHeights; } }); Header.DELAY_SYNC_CHECK = DELAY_SYNC_CHECK; module.exports = Header;