devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
687 lines (686 loc) • 29.3 kB
JavaScript
/**
* DevExtreme (esm/ui/grid_core/ui.grid_core.grid_view.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import $ from "../../core/renderer";
import modules from "./ui.grid_core.modules";
import {
deferRender,
deferUpdate
} from "../../core/utils/common";
import {
hasWindow,
getWindow
} from "../../core/utils/window";
import {
each
} from "../../core/utils/iterator";
import {
isString,
isDefined,
isNumeric
} from "../../core/utils/type";
import {
getBoundingRect
} from "../../core/utils/position";
import gridCoreUtils from "./ui.grid_core.utils";
import messageLocalization from "../../localization/message";
import {
when,
Deferred
} from "../../core/utils/deferred";
import domAdapter from "../../core/dom_adapter";
import browser from "../../core/utils/browser";
import * as accessibility from "../shared/accessibility";
var BORDERS_CLASS = "borders";
var TABLE_FIXED_CLASS = "table-fixed";
var IMPORTANT_MARGIN_CLASS = "important-margin";
var GRIDBASE_CONTAINER_CLASS = "dx-gridbase-container";
var HIDDEN_COLUMNS_WIDTH = "adaptiveHidden";
var VIEW_NAMES = ["columnsSeparatorView", "blockSeparatorView", "trackerView", "headerPanel", "columnHeadersView", "rowsView", "footerView", "columnChooserView", "filterPanelView", "pagerView", "draggingHeaderView", "contextMenuView", "errorView", "headerFilterView", "filterBuilderView"];
var isPercentWidth = function(width) {
return isString(width) && "%" === width.slice(-1)
};
var isPixelWidth = function(width) {
return isString(width) && "px" === width.slice(-2)
};
var getContainerHeight = function($container) {
var clientHeight = $container.get(0).clientHeight;
var paddingTop = parseFloat($container.css("paddingTop"));
var paddingBottom = parseFloat($container.css("paddingBottom"));
return clientHeight - paddingTop - paddingBottom
};
var calculateFreeWidth = function(that, widths) {
var contentWidth = that._rowsView.contentWidth();
var totalWidth = that._getTotalWidth(widths, contentWidth);
return contentWidth - totalWidth
};
var calculateFreeWidthWithCurrentMinWidth = function(that, columnIndex, currentMinWidth, widths) {
return calculateFreeWidth(that, widths.map((function(width, index) {
return index === columnIndex ? currentMinWidth : width
})))
};
var restoreFocus = function(focusedElement, selectionRange) {
accessibility.hiddenFocus(focusedElement);
gridCoreUtils.setSelectionRange(focusedElement, selectionRange)
};
var ResizingController = modules.ViewController.inherit({
_initPostRenderHandlers: function() {
var that = this;
var dataController = that._dataController;
if (!that._refreshSizesHandler) {
that._refreshSizesHandler = function(e) {
dataController.changed.remove(that._refreshSizesHandler);
var resizeDeferred;
var changeType = e && e.changeType;
var isDelayed = e && e.isDelayed;
var items = dataController.items();
if (!e || "refresh" === changeType || "prepend" === changeType || "append" === changeType) {
if (!isDelayed) {
resizeDeferred = that.resize()
}
} else if ("update" === changeType && e.changeTypes) {
if ((items.length > 1 || "insert" !== e.changeTypes[0]) && !(0 === items.length && "remove" === e.changeTypes[0]) && !e.needUpdateDimensions) {
deferUpdate((function() {
that._rowsView.resize()
}))
} else {
resizeDeferred = that.resize()
}
}
if (changeType && "updateSelection" !== changeType && "updateFocusedRow" !== changeType && !isDelayed) {
when(resizeDeferred).done((function() {
that._setAriaRowColCount();
that.fireContentReadyAction()
}))
}
};
that._dataController.changed.add((function() {
that._dataController.changed.add(that._refreshSizesHandler)
}))
}
},
fireContentReadyAction: function() {
this.component._fireContentReadyAction()
},
_setAriaRowColCount: function() {
var component = this.component;
component.setAria({
rowCount: this._dataController.totalItemsCount(),
colCount: component.columnCount()
}, component.$element().children("." + GRIDBASE_CONTAINER_CLASS))
},
_getBestFitWidths: function() {
var _widths;
var rowsView = this._rowsView;
var columnHeadersView = this._columnHeadersView;
var widths = rowsView.getColumnWidths();
if (!(null !== (_widths = widths) && void 0 !== _widths && _widths.length)) {
var _rowsView$getTableEle;
var headersTableElement = columnHeadersView.getTableElement();
columnHeadersView.setTableElement(null === (_rowsView$getTableEle = rowsView.getTableElement()) || void 0 === _rowsView$getTableEle ? void 0 : _rowsView$getTableEle.children(".dx-header"));
widths = columnHeadersView.getColumnWidths();
columnHeadersView.setTableElement(headersTableElement)
}
return widths
},
_setVisibleWidths: function(visibleColumns, widths) {
var columnsController = this._columnsController;
columnsController.beginUpdate();
each(visibleColumns, (function(index, column) {
var columnId = columnsController.getColumnId(column);
columnsController.columnOption(columnId, "visibleWidth", widths[index])
}));
columnsController.endUpdate()
},
_toggleBestFitModeForView: function(view, className, isBestFit) {
if (!view || !view.isVisible()) {
return
}
var $rowsTables = this._rowsView.getTableElements();
var $viewTables = view.getTableElements();
each($rowsTables, (index, tableElement) => {
var $tableBody;
var $rowsTable = $(tableElement);
var $viewTable = $viewTables.eq(index);
if ($viewTable && $viewTable.length) {
if (isBestFit) {
$tableBody = $viewTable.children("tbody").appendTo($rowsTable)
} else {
$tableBody = $rowsTable.children("." + className).appendTo($viewTable)
}
$tableBody.toggleClass(className, isBestFit);
$tableBody.toggleClass(this.addWidgetPrefix("best-fit"), isBestFit)
}
})
},
_toggleBestFitMode: function(isBestFit) {
var $rowsTable = this._rowsView.getTableElement();
var $rowsFixedTable = this._rowsView.getTableElements().eq(1);
if (!$rowsTable) {
return
}
$rowsTable.css("tableLayout", isBestFit ? "auto" : "fixed");
$rowsTable.children("colgroup").css("display", isBestFit ? "none" : "");
$rowsFixedTable.toggleClass(this.addWidgetPrefix(TABLE_FIXED_CLASS), !isBestFit);
this._toggleBestFitModeForView(this._columnHeadersView, "dx-header", isBestFit);
this._toggleBestFitModeForView(this._footerView, "dx-footer", isBestFit);
if (this._needStretch()) {
$rowsTable.get(0).style.width = isBestFit ? "auto" : ""
}
if (browser.msie && 11 === parseInt(browser.version)) {
$rowsTable.find("." + this.addWidgetPrefix(TABLE_FIXED_CLASS)).each((function() {
this.style.width = isBestFit ? "10px" : ""
}))
}
},
_synchronizeColumns: function() {
var columnsController = this._columnsController;
var visibleColumns = columnsController.getVisibleColumns();
var columnAutoWidth = this.option("columnAutoWidth");
var needBestFit = this._needBestFit();
var hasMinWidth = false;
var resetBestFitMode;
var isColumnWidthsCorrected = false;
var resultWidths = [];
var focusedElement;
var selectionRange;
!needBestFit && each(visibleColumns, (function(index, column) {
if ("auto" === column.width) {
needBestFit = true;
return false
}
}));
each(visibleColumns, (function(index, column) {
if (column.minWidth) {
hasMinWidth = true;
return false
}
}));
this._setVisibleWidths(visibleColumns, []);
if (needBestFit) {
focusedElement = domAdapter.getActiveElement();
selectionRange = gridCoreUtils.getSelectionRange(focusedElement);
this._toggleBestFitMode(true);
resetBestFitMode = true
}
var $element = this.component.$element();
if ($element && $element[0] && this._maxWidth) {
delete this._maxWidth;
$element[0].style.maxWidth = ""
}
deferUpdate(() => {
if (needBestFit) {
resultWidths = this._getBestFitWidths();
each(visibleColumns, (function(index, column) {
var columnId = columnsController.getColumnId(column);
columnsController.columnOption(columnId, "bestFitWidth", resultWidths[index], true)
}))
} else if (hasMinWidth) {
resultWidths = this._getBestFitWidths()
}
each(visibleColumns, (function(index) {
var width = this.width;
if ("auto" !== width) {
if (isDefined(width)) {
resultWidths[index] = isNumeric(width) || isPixelWidth(width) ? parseFloat(width) : width
} else if (!columnAutoWidth) {
resultWidths[index] = void 0
}
}
}));
if (resetBestFitMode) {
this._toggleBestFitMode(false);
resetBestFitMode = false;
if (focusedElement && focusedElement !== domAdapter.getActiveElement()) {
var isFocusOutsideWindow = getBoundingRect(focusedElement).bottom < 0;
if (!isFocusOutsideWindow) {
if (browser.msie) {
setTimeout((function() {
restoreFocus(focusedElement, selectionRange)
}))
} else {
restoreFocus(focusedElement, selectionRange)
}
}
}
}
isColumnWidthsCorrected = this._correctColumnWidths(resultWidths, visibleColumns);
if (columnAutoWidth) {
! function() {
var expandColumnWidth;
each(visibleColumns, (function(index, column) {
if ("groupExpand" === column.type) {
expandColumnWidth = resultWidths[index]
}
}));
each(visibleColumns, (function(index, column) {
if ("groupExpand" === column.type && expandColumnWidth) {
resultWidths[index] = expandColumnWidth
}
}))
}();
if (this._needStretch()) {
this._processStretch(resultWidths, visibleColumns)
}
}
deferRender(() => {
if (needBestFit || isColumnWidthsCorrected) {
this._setVisibleWidths(visibleColumns, resultWidths)
}
})
})
},
_needBestFit: function() {
return this.option("columnAutoWidth")
},
_needStretch: function() {
return this._columnsController.getVisibleColumns().some(c => "auto" === c.width && !c.command)
},
_getAverageColumnsWidth: function(resultWidths) {
var freeWidth = calculateFreeWidth(this, resultWidths);
var columnCountWithoutWidth = resultWidths.filter((function(width) {
return void 0 === width
})).length;
return freeWidth / columnCountWithoutWidth
},
_correctColumnWidths: function(resultWidths, visibleColumns) {
var that = this;
var i;
var hasPercentWidth = false;
var hasAutoWidth = false;
var isColumnWidthsCorrected = false;
var $element = that.component.$element();
var hasWidth = that._hasWidth;
var _loop = function() {
var index = i;
var column = visibleColumns[index];
var isHiddenColumn = resultWidths[index] === HIDDEN_COLUMNS_WIDTH;
var width = resultWidths[index];
var minWidth = column.minWidth;
if (minWidth) {
if (void 0 === width) {
var averageColumnsWidth = that._getAverageColumnsWidth(resultWidths);
width = averageColumnsWidth
} else if (isPercentWidth(width)) {
var freeWidth = calculateFreeWidthWithCurrentMinWidth(that, index, minWidth, resultWidths);
if (freeWidth < 0) {
width = -1
}
}
}
var realColumnWidth = that._getRealColumnWidth(index, resultWidths.map((function(columnWidth, columnIndex) {
return index === columnIndex ? width : columnWidth
})));
if (minWidth && !isHiddenColumn && realColumnWidth < minWidth) {
resultWidths[index] = minWidth;
isColumnWidthsCorrected = true;
i = -1
}
if (!isDefined(column.width)) {
hasAutoWidth = true
}
if (isPercentWidth(column.width)) {
hasPercentWidth = true
}
};
for (i = 0; i < visibleColumns.length; i++) {
_loop()
}
if (!hasAutoWidth && resultWidths.length) {
var $rowsViewElement = that._rowsView.element();
var contentWidth = that._rowsView.contentWidth();
var scrollbarWidth = that._rowsView.getScrollbarWidth();
var totalWidth = that._getTotalWidth(resultWidths, contentWidth);
if (totalWidth < contentWidth) {
var lastColumnIndex = gridCoreUtils.getLastResizableColumnIndex(visibleColumns, resultWidths);
if (lastColumnIndex >= 0) {
resultWidths[lastColumnIndex] = "auto";
isColumnWidthsCorrected = true;
if (false === hasWidth && !hasPercentWidth) {
var borderWidth = that.option("showBorders") ? Math.ceil($rowsViewElement.outerWidth() - $rowsViewElement.innerWidth()) : 0;
that._maxWidth = totalWidth + scrollbarWidth + borderWidth;
$element.css("maxWidth", that._maxWidth)
}
}
}
}
return isColumnWidthsCorrected
},
_processStretch: function(resultSizes, visibleColumns) {
var groupSize = this._rowsView.contentWidth();
var tableSize = this._getTotalWidth(resultSizes, groupSize);
var unusedIndexes = {
length: 0
};
if (!resultSizes.length) {
return
}
each(visibleColumns, (function(index) {
if (this.width || resultSizes[index] === HIDDEN_COLUMNS_WIDTH) {
unusedIndexes[index] = true;
unusedIndexes.length++
}
}));
var diff = groupSize - tableSize;
var diffElement = Math.floor(diff / (resultSizes.length - unusedIndexes.length));
var onePixelElementsCount = diff - diffElement * (resultSizes.length - unusedIndexes.length);
if (diff >= 0) {
for (var i = 0; i < resultSizes.length; i++) {
if (unusedIndexes[i]) {
continue
}
resultSizes[i] += diffElement;
if (onePixelElementsCount > 0) {
if (onePixelElementsCount < 1) {
resultSizes[i] += onePixelElementsCount;
onePixelElementsCount = 0
} else {
resultSizes[i]++;
onePixelElementsCount--
}
}
}
}
},
_getRealColumnWidth: function(columnIndex, columnWidths, groupWidth) {
var ratio = 1;
var width = columnWidths[columnIndex];
if (!isPercentWidth(width)) {
return parseFloat(width)
}
var percentTotalWidth = columnWidths.reduce((sum, width, index) => {
if (!isPercentWidth(width)) {
return sum
}
return sum + parseFloat(width)
}, 0);
var pixelTotalWidth = columnWidths.reduce((sum, width) => {
if (!width || width === HIDDEN_COLUMNS_WIDTH || isPercentWidth(width)) {
return sum
}
return sum + parseFloat(width)
}, 0);
groupWidth = groupWidth || this._rowsView.contentWidth();
var freeSpace = groupWidth - pixelTotalWidth;
var percentTotalWidthInPixel = percentTotalWidth * groupWidth / 100;
if (pixelTotalWidth > 0 && percentTotalWidthInPixel + pixelTotalWidth >= groupWidth) {
ratio = percentTotalWidthInPixel > freeSpace ? freeSpace / percentTotalWidthInPixel : 1
}
return parseFloat(width) * groupWidth * ratio / 100
},
_getTotalWidth: function(widths, groupWidth) {
var result = 0;
for (var i = 0; i < widths.length; i++) {
var width = widths[i];
if (width && width !== HIDDEN_COLUMNS_WIDTH) {
result += this._getRealColumnWidth(i, widths, groupWidth)
}
}
return Math.ceil(result)
},
updateSize: function(rootElement) {
var $groupElement;
var width;
var $rootElement = $(rootElement);
var importantMarginClass = this.addWidgetPrefix(IMPORTANT_MARGIN_CLASS);
if (void 0 === this._hasHeight && $rootElement && $rootElement.is(":visible") && $rootElement.width()) {
$groupElement = $rootElement.children("." + this.getWidgetContainerClass());
if ($groupElement.length) {
$groupElement.detach()
}
this._hasHeight = !!getContainerHeight($rootElement);
width = $rootElement.width();
$rootElement.addClass(importantMarginClass);
this._hasWidth = $rootElement.width() === width;
$rootElement.removeClass(importantMarginClass);
if ($groupElement.length) {
$groupElement.appendTo($rootElement)
}
}
},
publicMethods: function() {
return ["resize", "updateDimensions"]
},
resize: function() {
return !this.component._requireResize && this.updateDimensions()
},
updateDimensions: function(checkSize) {
var that = this;
that._initPostRenderHandlers();
if (!that._checkSize(checkSize)) {
return
}
var prevResult = that._resizeDeferred;
var result = that._resizeDeferred = new Deferred;
when(prevResult).always((function() {
deferRender((function() {
if (that._dataController.isLoaded()) {
that._synchronizeColumns()
}
that._resetGroupElementHeight();
deferUpdate((function() {
deferRender((function() {
deferUpdate((function() {
that._updateDimensionsCore()
}))
}))
}))
})).done(result.resolve).fail(result.reject)
}));
return result.promise()
},
_resetGroupElementHeight: function() {
var groupElement = this.component.$element().children().get(0);
var scrollable = this._rowsView.getScrollable();
if (groupElement && groupElement.style.height && (!scrollable || !scrollable.scrollTop())) {
groupElement.style.height = ""
}
},
_checkSize: function(checkSize) {
var $rootElement = this.component.$element();
if (checkSize && (this._lastWidth === $rootElement.width() && this._lastHeight === $rootElement.height() && this._devicePixelRatio === getWindow().devicePixelRatio || !$rootElement.is(":visible"))) {
return false
}
return true
},
_setScrollerSpacingCore: function(hasHeight) {
var that = this;
var vScrollbarWidth = hasHeight ? that._rowsView.getScrollbarWidth() : 0;
var hScrollbarWidth = that._rowsView.getScrollbarWidth(true);
deferRender((function() {
that._columnHeadersView && that._columnHeadersView.setScrollerSpacing(vScrollbarWidth);
that._footerView && that._footerView.setScrollerSpacing(vScrollbarWidth);
that._rowsView.setScrollerSpacing(vScrollbarWidth, hScrollbarWidth)
}))
},
_setScrollerSpacing: function(hasHeight) {
if (true === this.option("scrolling.useNative")) {
deferRender(() => {
deferUpdate(() => {
this._setScrollerSpacingCore(hasHeight)
})
})
} else {
this._setScrollerSpacingCore(hasHeight)
}
},
_updateDimensionsCore: function() {
var that = this;
var dataController = that._dataController;
var rowsView = that._rowsView;
var $rootElement = that.component.$element();
var groupElement = $rootElement.children().get(0);
var rootElementHeight = $rootElement && ($rootElement.get(0).clientHeight || $rootElement.height());
var maxHeight = parseInt($rootElement.css("maxHeight"));
var maxHeightHappened = maxHeight && rootElementHeight >= maxHeight;
var height = that.option("height") || $rootElement.get(0).style.height;
var editorFactory = that.getController("editorFactory");
var isMaxHeightApplied = maxHeightHappened && groupElement.scrollHeight === groupElement.offsetHeight;
var $testDiv;
that.updateSize($rootElement);
var hasHeight = that._hasHeight || maxHeightHappened;
if (height && that._hasHeight ^ "auto" !== height) {
$testDiv = $("<div>").height(height).appendTo($rootElement);
that._hasHeight = !!$testDiv.height();
$testDiv.remove()
}
deferRender((function() {
rowsView.height(null, hasHeight);
if (maxHeightHappened && !isMaxHeightApplied) {
$(groupElement).css("height", maxHeight)
}
if (!dataController.isLoaded()) {
rowsView.setLoading(dataController.isLoading());
return
}
deferUpdate((function() {
that._updateLastSizes($rootElement);
that._setScrollerSpacing(hasHeight);
each(VIEW_NAMES, (function(index, viewName) {
var view = that.getView(viewName);
if (view) {
view.resize()
}
}));
editorFactory && editorFactory.resize()
}))
}))
},
_updateLastSizes: function($rootElement) {
this._lastWidth = $rootElement.width();
this._lastHeight = $rootElement.height();
this._devicePixelRatio = getWindow().devicePixelRatio
},
optionChanged: function(args) {
switch (args.name) {
case "width":
case "height":
this.component._renderDimensions();
this.resize();
case "renderAsync":
args.handled = true;
return;
default:
this.callBase(args)
}
},
init: function() {
this._dataController = this.getController("data");
this._columnsController = this.getController("columns");
this._columnHeadersView = this.getView("columnHeadersView");
this._footerView = this.getView("footerView");
this._rowsView = this.getView("rowsView")
}
});
var SynchronizeScrollingController = modules.ViewController.inherit({
_scrollChangedHandler: function(views, pos, viewName) {
for (var j = 0; j < views.length; j++) {
if (views[j] && views[j].name !== viewName) {
views[j].scrollTo({
left: pos.left,
top: pos.top
})
}
}
},
init: function() {
var views = [this.getView("columnHeadersView"), this.getView("footerView"), this.getView("rowsView")];
for (var i = 0; i < views.length; i++) {
var view = views[i];
if (view) {
view.scrollChanged.add(this._scrollChangedHandler.bind(this, views))
}
}
}
});
var GridView = modules.View.inherit({
_endUpdateCore: function() {
if (this.component._requireResize) {
this.component._requireResize = false;
this._resizingController.resize()
}
},
_getWidgetAriaLabel: function() {
return "dxDataGrid-ariaDataGrid"
},
init: function() {
this._resizingController = this.getController("resizing");
this._dataController = this.getController("data")
},
getView: function(name) {
return this.component._views[name]
},
element: function() {
return this._groupElement
},
optionChanged: function(args) {
if (isDefined(this._groupElement) && "showBorders" === args.name) {
this._groupElement.toggleClass(this.addWidgetPrefix(BORDERS_CLASS), !!args.value);
args.handled = true
} else {
this.callBase(args)
}
},
_renderViews: function($groupElement) {
var that = this;
each(VIEW_NAMES, (function(index, viewName) {
var view = that.getView(viewName);
if (view) {
view.render($groupElement)
}
}))
},
_getTableRoleName: function() {
return "grid"
},
render: function($rootElement) {
var isFirstRender = !this._groupElement;
var $groupElement = this._groupElement || $("<div>").addClass(this.getWidgetContainerClass());
$groupElement.addClass(GRIDBASE_CONTAINER_CLASS);
$groupElement.toggleClass(this.addWidgetPrefix(BORDERS_CLASS), !!this.option("showBorders"));
this.setAria("role", "presentation", $rootElement);
this.component.setAria({
role: this._getTableRoleName(),
label: messageLocalization.format(this._getWidgetAriaLabel())
}, $groupElement);
this._rootElement = $rootElement || this._rootElement;
if (isFirstRender) {
this._groupElement = $groupElement;
hasWindow() && this.getController("resizing").updateSize($rootElement);
$groupElement.appendTo($rootElement)
}
this._renderViews($groupElement)
},
update: function() {
var $rootElement = this._rootElement;
var $groupElement = this._groupElement;
var resizingController = this.getController("resizing");
if ($rootElement && $groupElement) {
resizingController.resize();
if (this._dataController.isLoaded()) {
this._resizingController.fireContentReadyAction()
}
}
}
});
export var gridViewModule = {
defaultOptions: function() {
return {
showBorders: false,
renderAsync: false
}
},
controllers: {
resizing: ResizingController,
synchronizeScrolling: SynchronizeScrollingController
},
views: {
gridView: GridView
},
VIEW_NAMES: VIEW_NAMES
};