devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
766 lines (654 loc) • 23.9 kB
JavaScript
"use strict";
var $ = require("../core/renderer"),
eventsEngine = require("../events/core/events_engine"),
commonUtils = require("../core/utils/common"),
typeUtils = require("../core/utils/type"),
errors = require("./widget/ui.errors"),
windowUtils = require("../core/utils/window"),
window = windowUtils.getWindow(),
iteratorUtils = require("../core/utils/iterator"),
extend = require("../core/utils/extend").extend,
registerComponent = require("../core/component_registrator"),
Box = require("./box"),
CollectionWidget = require("./collection/ui.collection_widget.edit");
var RESPONSIVE_BOX_CLASS = "dx-responsivebox",
SCREEN_SIZE_CLASS_PREFIX = RESPONSIVE_BOX_CLASS + "-screen-",
BOX_ITEM_CLASS = "dx-box-item",
BOX_ITEM_DATA_KEY = "dxBoxItemData",
HD_SCREEN_WIDTH = 1920;
/**
* @name dxResponsiveBox
* @publicName dxResponsiveBox
* @type object
* @inherits CollectionWidget
* @module ui/responsive_box
* @export default
*/
var ResponsiveBox = CollectionWidget.inherit({
_getDefaultOptions: function _getDefaultOptions() {
return extend(this.callBase(), {
/**
* @name dxResponsiveBoxOptions.rows
* @publicName rows
* @type Array<Object>
*/
/**
* @name dxResponsiveBoxOptions.rows.baseSize
* @publicName baseSize
* @type number | Enums.Mode
* @default 0
*/
/**
* @name dxResponsiveBoxOptions.rows.shrink
* @publicName shrink
* @type number
* @default 1
*/
/**
* @name dxResponsiveBoxOptions.rows.ratio
* @publicName ratio
* @type number
* @default 1
*/
/**
* @name dxResponsiveBoxOptions.rows.screen
* @publicName screen
* @type string
* @default undefined
*/
rows: [],
/**
* @name dxResponsiveBoxOptions.cols
* @publicName cols
* @type Array<Object>
*/
/**
* @name dxResponsiveBoxOptions.cols.baseSize
* @publicName baseSize
* @type number | Enums.Mode
* @default 0
*/
/**
* @name dxResponsiveBoxOptions.cols.shrink
* @publicName shrink
* @type number
* @default 1
*/
/**
* @name dxResponsiveBoxOptions.cols.ratio
* @publicName ratio
* @type number
* @default 1
*/
/**
* @name dxResponsiveBoxOptions.cols.screen
* @publicName screen
* @type string
* @default undefined
*/
cols: [],
/**
* @name dxResponsiveBoxOptions.screenByWidth
* @publicName screenByWidth
* @type function
* @default null
*/
screenByWidth: null,
/**
* @name dxResponsiveBoxOptions.singleColumnScreen
* @publicName singleColumnScreen
* @type string
* @default ""
*/
singleColumnScreen: "",
/**
* @name dxResponsiveBoxOptions.height
* @publicName height
* @default '100%'
* @inheritdoc
*/
height: "100%",
/**
* @name dxResponsiveBoxOptions.width
* @publicName width
* @default '100%'
* @inheritdoc
*/
width: "100%",
/**
* @name dxResponsiveBoxOptions.activeStateEnabled
* @publicName activeStateEnabled
* @hidden
* @inheritdoc
*/
activeStateEnabled: false,
/**
* @name dxResponsiveBoxOptions.focusStateEnabled
* @publicName focusStateEnabled
* @hidden
* @inheritdoc
*/
focusStateEnabled: false,
onItemStateChanged: undefined,
/**
* @name dxResponsiveBoxOptions.accessKey
* @publicName accessKey
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.hint
* @publicName hint
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.noDataText
* @publicName noDataText
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.onSelectionChanged
* @publicName onSelectionChanged
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.selectedIndex
* @publicName selectedIndex
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.selectedItem
* @publicName selectedItem
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.selectedItems
* @publicName selectedItems
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.selectedItemKeys
* @publicName selectedItemKeys
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.keyExpr
* @publicName keyExpr
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxOptions.tabIndex
* @publicName tabIndex
* @hidden
* @inheritdoc
*/
onLayoutChanged: null,
currentScreenFactor: undefined,
_layoutStrategy: undefined
});
},
_init: function _init() {
if (!this.option("screenByWidth")) {
this._options.screenByWidth = windowUtils.defaultScreenFactorFunc;
}
this.callBase();
this._initLayoutChangedAction();
},
_initLayoutChangedAction: function _initLayoutChangedAction() {
this._layoutChangedAction = this._createActionByOption("onLayoutChanged", {
excludeValidators: ["disabled", "readonly"]
});
},
_itemClass: function _itemClass() {
return BOX_ITEM_CLASS;
},
_itemDataKey: function _itemDataKey() {
return BOX_ITEM_DATA_KEY;
},
_initMarkup: function _initMarkup() {
this.callBase();
this.$element().addClass(RESPONSIVE_BOX_CLASS);
// NOTE: Fallback box strategy
this._updateRootBox();
},
_updateRootBox: function _updateRootBox() {
clearTimeout(this._updateTimer);
this._updateTimer = setTimeout(function () {
if (this._$root) {
eventsEngine.triggerHandler(this._$root, "dxupdate");
}
}.bind(this));
},
_renderItems: function _renderItems() {
this._setScreenSize();
this._screenItems = this._itemsByScreen();
this._prepareGrid();
this._spreadItems();
this._layoutItems();
this._linkNodeToItem();
},
_setScreenSize: function _setScreenSize() {
var currentScreen = this._getCurrentScreen();
this._removeScreenSizeClass();
this.$element().addClass(SCREEN_SIZE_CLASS_PREFIX + currentScreen);
this.option("currentScreenFactor", currentScreen);
},
_removeScreenSizeClass: function _removeScreenSizeClass() {
var currentScreenFactor = this.option("currentScreenFactor");
currentScreenFactor && this.$element().removeClass(SCREEN_SIZE_CLASS_PREFIX + currentScreenFactor);
},
_prepareGrid: function _prepareGrid() {
var grid = this._grid = [];
this._prepareRowsAndCols();
iteratorUtils.each(this._rows, function () {
var row = [];
grid.push(row);
iteratorUtils.each(this._cols, function () {
row.push(this._createEmptyCell());
}.bind(this));
}.bind(this));
},
_prepareRowsAndCols: function _prepareRowsAndCols() {
if (this._isSingleColumnScreen()) {
this._prepareSingleColumnScreenItems();
this._rows = this._defaultSizeConfig(this._screenItems.length);
this._cols = this._defaultSizeConfig(1);
} else {
this._rows = this._sizesByScreen(this.option("rows"));
this._cols = this._sizesByScreen(this.option("cols"));
}
},
_isSingleColumnScreen: function _isSingleColumnScreen() {
return this._screenRegExp().test(this.option("singleColumnScreen")) || !this.option("rows").length || !this.option("cols").length;
},
_prepareSingleColumnScreenItems: function _prepareSingleColumnScreenItems() {
this._screenItems.sort(function (item1, item2) {
return item1.location.row - item2.location.row || item1.location.col - item2.location.col;
});
iteratorUtils.each(this._screenItems, function (index, item) {
extend(item.location, { row: index, col: 0, rowspan: 1, colspan: 1 });
});
},
_sizesByScreen: function _sizesByScreen(sizeConfigs) {
return iteratorUtils.map(this._filterByScreen(sizeConfigs), function (sizeConfig) {
return extend(this._defaultSizeConfig(), sizeConfig);
}.bind(this));
},
_defaultSizeConfig: function _defaultSizeConfig(size) {
var defaultSizeConfig = { ratio: 1, baseSize: 0, minSize: 0, maxSize: 0 };
if (!arguments.length) {
return defaultSizeConfig;
}
var result = [];
for (var i = 0; i < size; i++) {
result.push(defaultSizeConfig);
}
return result;
},
_filterByScreen: function _filterByScreen(items) {
var screenRegExp = this._screenRegExp();
return commonUtils.grep(items, function (item) {
return !item.screen || screenRegExp.test(item.screen);
});
},
_screenRegExp: function _screenRegExp() {
var screen = this._getCurrentScreen();
return new RegExp("(^|\\s)" + screen + "($|\\s)", "i");
},
_getCurrentScreen: function _getCurrentScreen() {
var width = this._screenWidth();
return this.option("screenByWidth")(width);
},
_screenWidth: function _screenWidth() {
return windowUtils.hasWindow() ? $(window).width() : HD_SCREEN_WIDTH;
},
_createEmptyCell: function _createEmptyCell() {
return {
item: {},
location: { colspan: 1, rowspan: 1 }
};
},
_spreadItems: function _spreadItems() {
iteratorUtils.each(this._screenItems, function (_, itemInfo) {
var location = itemInfo.location || {};
var itemCol = location.col;
var itemRow = location.row;
var row = this._grid[itemRow];
var itemCell = row && row[itemCol];
this._occupyCells(itemCell, itemInfo);
}.bind(this));
},
_itemsByScreen: function _itemsByScreen() {
return iteratorUtils.map(this.option("items"), function (item) {
var locations = item.location || {};
locations = typeUtils.isPlainObject(locations) ? [locations] : locations;
return iteratorUtils.map(this._filterByScreen(locations), function (location) {
return {
item: item,
location: extend({ rowspan: 1, colspan: 1 }, location)
};
});
}.bind(this));
},
_occupyCells: function _occupyCells(itemCell, itemInfo) {
if (!itemCell || this._isItemCellOccupied(itemCell, itemInfo)) {
return;
}
extend(itemCell, itemInfo);
this._markSpanningCell(itemCell);
},
_isItemCellOccupied: function _isItemCellOccupied(itemCell, itemInfo) {
if (!typeUtils.isEmptyObject(itemCell.item)) {
return true;
}
var result = false;
this._loopOverSpanning(itemInfo.location, function (cell) {
result = result || !typeUtils.isEmptyObject(cell.item);
});
return result;
},
_loopOverSpanning: function _loopOverSpanning(location, callback) {
var rowEnd = location.row + location.rowspan - 1;
var colEnd = location.col + location.colspan - 1;
var boundRowEnd = Math.min(rowEnd, this._rows.length - 1);
var boundColEnd = Math.min(colEnd, this._cols.length - 1);
location.rowspan -= rowEnd - boundRowEnd;
location.colspan -= colEnd - boundColEnd;
for (var rowIndex = location.row; rowIndex <= boundRowEnd; rowIndex++) {
for (var colIndex = location.col; colIndex <= boundColEnd; colIndex++) {
if (rowIndex !== location.row || colIndex !== location.col) {
callback(this._grid[rowIndex][colIndex]);
}
}
}
},
_markSpanningCell: function _markSpanningCell(itemCell) {
this._loopOverSpanning(itemCell.location, function (cell) {
extend(cell, {
item: itemCell.item,
spanningCell: itemCell
});
});
},
_linkNodeToItem: function _linkNodeToItem() {
iteratorUtils.each(this._itemElements(), function (_, itemNode) {
var $item = $(itemNode),
item = $item.data(BOX_ITEM_DATA_KEY);
if (!item.box) {
item.node = $item.children();
}
});
},
_layoutItems: function _layoutItems() {
var rowsCount = this._grid.length;
var colsCount = rowsCount && this._grid[0].length;
if (!rowsCount && !colsCount) {
return;
}
var result = this._layoutBlock({
direction: "col",
row: { start: 0, end: rowsCount - 1 },
col: { start: 0, end: colsCount - 1 }
});
var rootBox = this._prepareBoxConfig(result.box || { direction: "row", items: [extend(result, { ratio: 1 })] });
extend(rootBox, this._rootBoxConfig(rootBox.items));
this._$root = $("<div>").appendTo(this._itemContainer());
this._createComponent(this._$root, Box, rootBox);
},
_rootBoxConfig: function _rootBoxConfig(items) {
var rootItems = iteratorUtils.each(items, function (index, item) {
this._needApplyAutoBaseSize(item) && extend(item, { baseSize: "auto" });
}.bind(this));
return extend({
width: "100%",
height: "100%",
items: rootItems,
itemTemplate: this._getTemplateByOption("itemTemplate"),
itemHoldTimeout: this.option("itemHoldTimeout"),
onItemHold: this._createActionByOption("onItemHold"),
onItemClick: this._createActionByOption("onItemClick"),
onItemContextMenu: this._createActionByOption("onItemContextMenu"),
onItemRendered: this._createActionByOption("onItemRendered")
}, { _layoutStrategy: this.option("_layoutStrategy") });
},
_needApplyAutoBaseSize: function _needApplyAutoBaseSize(item) {
return !item.baseSize && (!item.minSize || item.minSize === "auto") && (!item.maxSize || item.maxSize === "auto");
},
_prepareBoxConfig: function _prepareBoxConfig(config) {
return extend(config || {}, {
crossAlign: "stretch",
onItemStateChanged: this.option("onItemStateChanged")
});
},
_layoutBlock: function _layoutBlock(options) {
if (this._isSingleItem(options)) {
return this._itemByCell(options.row.start, options.col.start);
}
return this._layoutDirection(options);
},
_isSingleItem: function _isSingleItem(options) {
var firstCellLocation = this._grid[options.row.start][options.col.start].location;
var isItemRowSpanned = options.row.end - options.row.start === firstCellLocation.rowspan - 1;
var isItemColSpanned = options.col.end - options.col.start === firstCellLocation.colspan - 1;
return isItemRowSpanned && isItemColSpanned;
},
_itemByCell: function _itemByCell(rowIndex, colIndex) {
var itemCell = this._grid[rowIndex][colIndex];
return itemCell.spanningCell ? null : itemCell.item;
},
_layoutDirection: function _layoutDirection(options) {
var items = [];
var direction = options.direction;
var crossDirection = this._crossDirection(direction);
var block;
while (block = this._nextBlock(options)) {
if (this._isBlockIndivisible(options.prevBlockOptions, block)) {
throw errors.Error("E1025");
}
var item = this._layoutBlock({
direction: crossDirection,
row: block.row,
col: block.col,
prevBlockOptions: options
});
if (item) {
extend(item, this._blockSize(block, crossDirection));
items.push(item);
}
options[crossDirection].start = block[crossDirection].end + 1;
}
return {
box: this._prepareBoxConfig({ direction: direction, items: items })
};
},
_isBlockIndivisible: function _isBlockIndivisible(options, block) {
return options && options.col.start === block.col.start && options.col.end === block.col.end && options.row.start === block.row.start && options.row.end === block.row.end;
},
_crossDirection: function _crossDirection(direction) {
return direction === "col" ? "row" : "col";
},
_nextBlock: function _nextBlock(options) {
var direction = options.direction;
var crossDirection = this._crossDirection(direction);
var startIndex = options[direction].start;
var endIndex = options[direction].end;
var crossStartIndex = options[crossDirection].start;
if (crossStartIndex > options[crossDirection].end) {
return null;
}
var crossSpan = 1;
for (var crossIndex = crossStartIndex; crossIndex < crossStartIndex + crossSpan; crossIndex++) {
var lineCrossSpan = 1;
for (var index = startIndex; index <= endIndex; index++) {
var cell = this._cellByDirection(direction, index, crossIndex);
lineCrossSpan = Math.max(lineCrossSpan, cell.location[crossDirection + "span"]);
}
var lineCrossEndIndex = crossIndex + lineCrossSpan;
var crossEndIndex = crossStartIndex + crossSpan;
if (lineCrossEndIndex > crossEndIndex) {
crossSpan += lineCrossEndIndex - crossEndIndex;
}
}
var result = {};
result[direction] = { start: startIndex, end: endIndex };
result[crossDirection] = { start: crossStartIndex, end: crossStartIndex + crossSpan - 1 };
return result;
},
_cellByDirection: function _cellByDirection(direction, index, crossIndex) {
return direction === "col" ? this._grid[crossIndex][index] : this._grid[index][crossIndex];
},
_blockSize: function _blockSize(block, direction) {
var sizeConfigs = direction === "row" ? this._rows : this._cols;
var result = {
ratio: 0,
baseSize: 0,
minSize: 0,
maxSize: 0
};
for (var index = block[direction].start; index <= block[direction].end; index++) {
var sizeConfig = sizeConfigs[index];
result.ratio += sizeConfig.ratio;
result.baseSize += sizeConfig.baseSize;
result.minSize += sizeConfig.minSize;
result.maxSize += sizeConfig.maxSize;
}
result.minSize = result.minSize ? result.minSize : "auto";
result.maxSize = result.maxSize ? result.maxSize : "auto";
this._isSingleColumnScreen() && (result.baseSize = 'auto');
return result;
},
_update: function _update() {
var $existingRoot = this._$root;
this._renderItems();
$existingRoot && $existingRoot.detach();
this._saveAssistantRoot($existingRoot);
this._layoutChangedAction();
this._updateRootBox();
},
_saveAssistantRoot: function _saveAssistantRoot($root) {
this._assistantRoots = this._assistantRoots || [];
this._assistantRoots.push($root);
},
_dispose: function _dispose() {
clearTimeout(this._updateTimer);
this._cleanUnusedRoots();
this.callBase.apply(this, arguments);
},
_cleanUnusedRoots: function _cleanUnusedRoots() {
if (!this._assistantRoots) {
return;
}
iteratorUtils.each(this._assistantRoots, function (_, item) {
$(item).remove();
});
},
_clearItemNodeTemplates: function _clearItemNodeTemplates() {
iteratorUtils.each(this.option("items"), function () {
delete this.node;
});
},
_toggleVisibility: function _toggleVisibility(visible) {
this.callBase(visible);
if (visible) {
this._updateRootBox();
}
},
_attachClickEvent: commonUtils.noop,
_optionChanged: function _optionChanged(args) {
switch (args.name) {
case "rows":
case "cols":
case "screenByWidth":
case "_layoutStrategy":
case "singleColumnScreen":
this._clearItemNodeTemplates();
this._invalidate();
break;
case "width":
case "height":
this.callBase(args);
this._update();
break;
case "onLayoutChanged":
this._initLayoutChangedAction();
break;
case "itemTemplate":
this._clearItemNodeTemplates();
this.callBase(args);
break;
case "currentScreenFactor":
break;
default:
this.callBase(args);
}
},
_dimensionChanged: function _dimensionChanged() {
if (this._getCurrentScreen() !== this.option("currentScreenFactor")) {
this._update();
}
},
repaint: function repaint() {
this._update();
}
/**
* @name dxResponsiveBoxMethods.registerKeyHandler
* @publicName registerKeyHandler(key, handler)
* @hidden
* @inheritdoc
*/
/**
* @name dxResponsiveBoxMethods.focus
* @publicName focus()
* @hidden
* @inheritdoc
*/
});
/**
* @name dxResponsiveBoxItemTemplate
* @publicName dxResponsiveBoxItemTemplate
* @inherits CollectionWidgetItemTemplate
* @type object
*/
/**
* @name dxResponsiveBoxItemTemplate.location
* @publicName location
* @type Object|Array<Object>
*/
/**
* @name dxResponsiveBoxItemTemplate.location.row
* @publicName row
* @type number
*/
/**
* @name dxResponsiveBoxItemTemplate.location.col
* @publicName col
* @type number
*/
/**
* @name dxResponsiveBoxItemTemplate.location.rowspan
* @publicName rowspan
* @type number
* @default undefined
*/
/**
* @name dxResponsiveBoxItemTemplate.location.colspan
* @publicName colspan
* @type number
* @default undefined
*/
/**
* @name dxResponsiveBoxItemTemplate.location.screen
* @publicName screen
* @type string
* @default undefined
*/
registerComponent("dxResponsiveBox", ResponsiveBox);
module.exports = ResponsiveBox;