/**
* @fileoverview
* A grid to contain multiple float widgets.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /xbrowsers/kekule.x.js
* requires /core/kekule.common.js
* requires /widgets/kekule.widget.base.js
* requires /widgets/kekule.widget.helpers.js
* requires /widgets/operation/kekule.actions.js
* requires /widgets/commonCtrls/kekule.widget.buttons.js
* requires /widgets/commonCtrls/kekule.widget.containers.js
*
* requires /localization
*/
(function(){
"use strict";
var AU = Kekule.ArrayUtils;
var SU = Kekule.StyleUtils;
var DU = Kekule.DomUtils;
var EU = Kekule.HtmlElementUtils;
/** @ignore */
Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, {
WIDGET_GRID: 'K-Widget-Grid',
WIDGET_GRID_CELL: 'K-Widget-Grid-Cell',
WIDGET_GRID_ADD_CELL: 'K-Widget-Grid-Add-Cell',
WIDGET_GRID_INTERACTION_AREA: 'K-Widget-Grid-Interaction-Area',
WIDGET_GRID_WIDGET_PARENT: 'K-Widget-Grid-Widget-Parent',
WIDGET_GRID_BUTTON_REMOVE: 'K-Widget-Grid-Button-Remove',
WIDGET_GRID_ENABLE_CELL_INTERACTION: 'K-Widget-Grid-Enable-Cell-Interaction'
});
var CNS = Kekule.Widget.HtmlClassNames;
/**
* A grid to contain a series of child widgets.
* @class
* @augments Kekule.Widget.Container
*
* @property {String} cellWidth CSS width value for each cell.
* @property {String} cellHeight CSS height value for each cell.
* @property {Int} widgetPos Value from {@link Kekule.Widget.Position}.
* @property {Bool} autoShrinkWidgets Whether auto shrink large widgets to full fill the cell.
* @property {Bool} keepWidgetAspectRatio Whether keep the origin aspect ratio of widget when scaling.
* @property {Bool} restoreWidgetSizeOnHotTrack Whether restore the shrinked large widget's size when hover or click on cell.
* @property {Bool} enableAdd Whether shows an "adding widget" cell.
* @property {Bool} enableRemove Whether shows remove button on each cell.
*/
Kekule.Widget.WidgetGrid = Class.create(Kekule.Widget.Container,
/** @lends Kekule.Widget.WidgetGrid# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.WidgetGrid',
/** @private */
CELL_FIELD: '__$cell__',
/** @private */
WIDGET_FIELD: '__$cellWidget__',
/** @private */
INTERACTION_AREA_FIELD: '__$interactionArea__',
/** @private */
EXCEED_TOPRIGHT_FIELD: '__$exceedTopRight__',
/** @private */
BTN_REMOVE_CELL_FIELD: '__$removeCell__',
/** @construct */
initialize: function($super, parentOrElementOrDocument)
{
this._floatClearer = null;
this.reactCellMouseEnterBind = this.reactCellMouseEnter.bind(this);
this.reactCellMouseLeaveBind = this.reactCellMouseLeave.bind(this);
this.reactCellClickBind = this.reactCellClick.bind(this);
$super(parentOrElementOrDocument);
this.addEventListener('change', this.reactChildWidgetChange, this);
},
/** @private */
initProperties: function()
{
this.defineProp('childWidgetClass', {'dataType': DataType.CLASS, 'serializable': false});
this.defineProp('cellWidth', {'dataType': DataType.STRING
/*
'setter': function(value)
{
this.setPropStoreFieldValue('cellWidth', value);
this.updateAllCells();
}
*/
});
this.defineProp('cellHeight', {'dataType': DataType.STRING
/*
'setter': function(value)
{
this.setPropStoreFieldValue('cellHeight', value);
this.updateAllCells();
}
*/
});
this.defineProp('widgetPos', {'dataType': DataType.INT,
'enumSource': Kekule.Widget.Position
/*
'setter': function(value)
{
this.setPropStoreFieldValue('widgetPos', value);
this.updateAllCells();
}
*/
});
this.defineProp('autoShrinkWidgets', {'dataType': DataType.BOOL});
this.defineProp('keepWidgetAspectRatio', {'dataType': DataType.BOOL});
this.defineProp('restoreWidgetSizeOnHotTrack', {'dataType': DataType.BOOL});
this.defineProp('enableAdd', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('enableAdd', value);
if (value)
this.setAddingCell(this.createAddCell());
else
{
var cell = this.getAddingCell();
if (cell)
this.getContainerElement().removeChild(cell);
}
}
});
this.defineProp('enableRemove', {'dataType': DataType.BOOL,
'getter': function()
{
return this.hasClassName(CNS.WIDGET_GRID_ENABLE_CELL_INTERACTION);
},
'setter': function(value)
{
if (value)
this.addClassName(CNS.WIDGET_GRID_ENABLE_CELL_INTERACTION);
else
this.removeClassName(CNS.WIDGET_GRID_ENABLE_CELL_INTERACTION);
}
});
this.defineProp('hotCell', {'dataType': DataType.OBJECT, 'serializable': false,
'setter': function(value)
{
if (this.getHotCell() !== value)
{
this.hotCellChanged(this.getHotCell(), value);
this.setPropStoreFieldValue('hotCell', value);
}
}
}); // private
this.defineProp('addingCell', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
/** @ignore */
initPropValues: function()
{
this.setPropStoreFieldValue('autoShrinkWidgets', true);
this.setPropStoreFieldValue('keepWidgetAspectRatio', true);
this.setPropStoreFieldValue('restoreWidgetSizeOnHotTrack', true);
this.setEnableRemove(true);
this.setUseCornerDecoration(true);
},
/** @ignore */
doGetWidgetClassName: function($super)
{
return $super() + ' ' + CNS.WIDGET_GRID;
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('div');
return result;
},
/** @ignore */
doSetUseCornerDecoration: function($super, value)
{
$super(value);
this.updateAllCells();
},
/** @ignore */
doObjectChange: function($super, modifiedPropNames)
{
$super(modifiedPropNames);
var inter = AU.intersect(modifiedPropNames, ['autoShrinkWidgets', 'keepWidgetAspectRatio', 'cellWidth', 'cellHeight', 'widgetPos']);
if (inter.length > 0)
this.updateAllCells();
},
/** @ignore */
doWidgetShowStateChanged: function($super, isShown)
{
if (isShown)
this.updateAllCells();
return $super(isShown);
},
/**
* Call function to each widgets
* @param {Func} callFunc callFunc(widget)
*/
each: function(callFunc)
{
var ws = Kekule.ArrayUtils.clone(this.getChildWidgets());
for (var i = 0, l = ws.length; i < l; ++i)
{
callFunc(ws[i]);
}
},
/**
* Add a widget to grid.
* This method will also set widget's parent to grid.
* @param {Kekule.Widget.BaseWidget} widget
*/
addWidget: function(widget)
{
widget.setParent(this);
},
/**
* Remove a widget from grid.
* @param {Kekule.Widget.BaseWidget} widget
* @param {Bool} doFinalize
*/
removeWidget: function(widget, doFinalize)
{
if (!this.hasChild(widget))
return;
widget.setParent(null);
if (doFinalize)
widget.finalize();
},
/**
* Create a new default child widget.
* @returns {Kekule.Widget.BaseWidget}
*/
createWidget: function()
{
var result = this.doCreateNewChildWidget(this.getDocument());
if (result)
{
result.setParent(this);
return result;
}
},
/**
* Create a new widget by adding cell.
* Descendants need to override this method/
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
doCreateNewChildWidget: function(doc)
{
return null;
//return new Kekule.Widget.Button(doc, 'hi'); // debug
},
/** @private */
getFloatClearer: function()
{
if (!this._floatClearer)
{
this._floatClearer = this.getDocument().createElement('div');
this._floatClearer.style.clear = 'both';
this.getContainerElement().appendChild(this._floatClearer);
}
return this._floatClearer;
},
/**
* Create a cell element to contains the child widget.
* @param {Kekule.Widget.BaseWidget} widget
* @returns {HTMLElement}
* @private
*/
createCell: function(widget)
{
var isAddCell = !widget;
/*
if (isAddCell)
widget = new Kekule.Widget.Button(this.getDocument(), 'Add');
*/
var doc = this.getDocument();
var cell = doc.createElement('div');
cell.className = CNS.WIDGET_GRID_CELL + ' ' + CNS.DYN_CREATED + (isAddCell? ' ' + CNS.WIDGET_GRID_ADD_CELL: '');
cell[this.WIDGET_FIELD] = widget;
Kekule.X.Event.addListener(cell, 'mouseenter', this.reactCellMouseEnterBind);
Kekule.X.Event.addListener(cell, 'mouseleave', this.reactCellMouseLeaveBind);
Kekule.X.Event.addListener(cell, 'click', this.reactCellClickBind);
/*
var interElem = doc.createElement('div');
interElem.className = CNS.WIDGET_GRID_INTERACTION_LAYER + ' ' + CNS.DYN_CREATED;
cell.appendChild(interElem);
*/
var p = doc.createElement('div');
p.className = CNS.WIDGET_GRID_WIDGET_PARENT + ' ' + CNS.DYN_CREATED;
p[this.WIDGET_FIELD] = widget;
//interElem.appendChild(p);
if (isAddCell)
{
p.innerHTML = Kekule.$L('WidgetTexts.CAPTION_ADD_CELL'); //Kekule.WidgetTexts.CAPTION_ADD_CELL;
p.title = Kekule.$L('WidgetTexts.HINT_ADD_CELL'); //Kekule.WidgetTexts.HINT_ADD_CELL;
}
cell.appendChild(p);
if (widget)
{
widget.appendToElem(p);
widget[this.CELL_FIELD] = cell;
}
if (!isAddCell)
{
var interElem = doc.createElement('div');
interElem.className = CNS.WIDGET_GRID_INTERACTION_AREA + ' ' + CNS.DYN_CREATED;
cell.appendChild(interElem);
p[this.INTERACTION_AREA_FIELD] = interElem;
cell[this.INTERACTION_AREA_FIELD] = interElem;
this.createCellInteractionWidgets(interElem, cell);
}
if (this.getAddingCell() && !isAddCell)
this.getContainerElement().insertBefore(cell, this.getAddingCell());
else
this.getContainerElement().insertBefore(cell, this.getFloatClearer());
this.updateCell(cell);
return cell;
},
/** @private */
createAddCell: function()
{
return this.createCell(null);
},
/** @private */
createCellInteractionWidgets: function(parentElem, cellElem)
{
var btn = new Kekule.Widget.Button(parentElem.ownerDocument);
btn.setText(/*Kekule.WidgetTexts.CAPTION_REMOVE_CELL*/Kekule.$L('WidgetTexts.CAPTION_REMOVE_CELL'));
btn.setHint(/*Kekule.WidgetTexts.HINT_REMOVE_CELL*/Kekule.$L('WidgetTexts.HINT_REMOVE_CELL'));
btn.addClassName(CNS.WIDGET_GRID_BUTTON_REMOVE);
btn[this.BTN_REMOVE_CELL_FIELD] = true;
var widget = this.getContainingWidget(cellElem);
btn[this.WIDGET_FIELD] = widget;
btn.setShowText(false);
btn.setShowGlyph(true);
btn.addEventListener('execute', function(e){
var btn = e.widget;
var widget = btn[this.WIDGET_FIELD];
this.removeWidget(widget, true);
}, this);
btn.appendToElem(parentElem);
},
/**
* Returns the cell element of widget.
* @param {Kekule.Widget.BaseWidget} widget
* @returns {HTMLElement}
*/
getWidgetCell: function(widget)
{
return widget[this.CELL_FIELD];
},
/**
* Returns the direct parent element of widget.
* @param {Kekule.Widget.BaseWidget} widget
* @returns {HTMLElement}
*/
getWidgetParentElem: function(widget)
{
var elem = widget.getElement();
return elem && elem.parentNode;
},
/** @private */
getWidgetParentElemOfCell: function(cell)
{
return cell.children[0];
},
/** @private */
getInteractionAreaElemOfCell: function(cell)
{
return cell[this.INTERACTION_AREA_FIELD];
},
/** @private */
getContainingWidget: function(elem)
{
return elem[this.WIDGET_FIELD];
},
/** @private */
childWidgetAdded: function($super, widget)
{
$super(widget);
this.createCell(widget);
},
/** @private */
childWidgetRemoved: function($super, widget)
{
$super(widget);
var cellElem = this.getWidgetCell(widget);
this.getContainerElement().removeChild(cellElem);
},
/** @private */
childWidgetMoved: function($super, widget, newIndex)
{
$super(widget, newIndex);
var elem = this.getWidgetCell(widget);
var refWidget = this.getChildWidgets()[newIndex + 1];
var refElem = refWidget? this.getWidgetCell(refWidget): null;
if (refElem)
this.getContainerElement().insertBefore(elem, refElem);
else
this.getContainerElement().appendChild(elem);
},
/** @private */
childrenModified: function($super)
{
// update float clearer
this.getContainerElement().appendChild(this.getFloatClearer());
},
/**
* Returns all cell elements.
* @returns {Array}
*/
getAllCells: function()
{
var widgets = this.getChildWidgets();
var result = [];
for (var i = 0, l = widgets.length; i < l; ++i)
{
result.push(this.getWidgetCell(widgets[i]));
}
// check if there is an add cell
if (this.getAddingCell())
{
result.push(this.getAddingCell());
}
return result;
},
/**
* Update width/height of cell by cellWidth/cellHeight property.
* @param {HTMLElement} cellElem
* @param {Bool} isHotCell;
* @private
*/
updateCell: function(cellElem, isHotCell)
{
var style = cellElem.style;
var w = this.getCellWidth();
var h = this.getCellHeight();
if (w)
style.width = w;
else
SU.removeStyleProperty(style, 'width');
if (h)
{
style.height = h;
//style.lineHeight = h; // enable vertical align
}
else
{
SU.removeStyleProperty(style, 'height');
//SU.removeStyleProperty(style, 'lineHeight');
}
// corner decoration
if (this.getUseCornerDecoration())
EU.addClass(cellElem, CNS.CORNER_ALL);
else
EU.removeClass(cellElem, CNS.CORNER_ALL);
// widget position
this.adjustCellWidgetPos(cellElem, isHotCell);
// interaction area
this.adjustInteractionArea(cellElem, isHotCell);
},
/** @private */
adjustCellWidgetPos: function(cellElem, isHotCell)
{
/*
var widget = this.getContainingWidget(cellElem);
if (!widget)
return;
var widgetElem = widget.getElement();
*/
//var containerElem = this.getWidgetParentElem(widget);
var containerElem = this.getWidgetParentElemOfCell(cellElem);
var widgetBound = EU.getElemOffsetDimension(containerElem);
var cellBound = EU.getElemClientDimension(cellElem); // without margin and border, but with padding
//console.log('cellSize', this.getCellWidth(), this.getCellHeight());
var paddingNames = ['top', 'right', 'bottom', 'left'];
var paddings = {};
for (var i = 0, l = paddingNames.length; i < l; ++i)
{
var sv = SU.getComputedStyle(cellElem, 'padding-' + paddingNames[i]);
paddings[paddingNames[i]] = SU.analysisUnitsValue(sv).value || 0;
}
var cellClientBound = Object.extend({}, cellBound); // bound without padding
cellClientBound.width -= (paddings.left + paddings.right);
cellClientBound.height -= (paddings.top + paddings.bottom);
var pos = this.getWidgetPos();
var WP = Kekule.Widget.Position;
// horizontal
var left = (pos & WP.LEFT)? paddings.left:
(pos & WP.RIGHT)? cellBound.width - widgetBound.width - paddings.right:
(cellClientBound.width - widgetBound.width) / 2 + paddings.left; // default, center
// vertical
var top = (pos & WP.TOP)? paddings.top:
(pos & WP.BOTTOM)? cellBound.height - widgetBound.height - paddings.bottom:
(cellClientBound.height - widgetBound.height) / 2 + paddings.top; // default, middle
//console.log('cellBound', cellBound, paddings, left, top);
containerElem.style.left = left + 'px';
containerElem.style.top = top + 'px';
var interArea = this.getInteractionAreaElemOfCell(cellElem);
if (interArea)
{
var interAreaBound = EU.getElemClientDimension(interArea);
var right = left + widgetBound.width;
var exceedTopRightCorner = (right + interAreaBound.width >= cellBound.width) && (top <= interAreaBound.height);
cellElem[this.EXCEED_TOPRIGHT_FIELD] = exceedTopRightCorner;
}
/*
// adjust interaction area, should be at top-right of cell
var interElem = this.getInteractionAreaElemOfCell(cellElem);
if (interElem)
{
var t = -top + paddings.top;
var right = left + widgetBound.width;
var r = (right - cellBound.width) + paddings.right;
interElem.style.top = t + 'px';
interElem.style.right = r + 'px';
}
*/
// check if shrink is essential
if ((this.getAutoShrinkWidgets() && !isHotCell) || !this.getRestoreWidgetSizeOnHotTrack())
{
var scaleX = (widgetBound.width > cellClientBound.width) ? cellClientBound.width / widgetBound.width : null;
var scaleY = (widgetBound.height > cellClientBound.height) ? cellClientBound.height / widgetBound.height : 1;
if (this.getKeepWidgetAspectRatio())
{
var scale = (scaleX && scaleY) ? Math.min(scaleX, scaleY) : scaleX || scaleY;
scaleX = scale;
scaleY = scale;
}
containerElem.style.transform = 'scale(' + scaleX + ', ' + scaleY + ')';
var scaleOriginX = (pos & WP.LEFT) ? '0' :
(pos & WP.RIGHT) ? '100%' :
'50%';
var scaleOriginY = (pos & WP.TOP) ? '0' :
(pos & WP.BOTTOM) ? '100%' :
'50%';
containerElem.style.transformOrigin = scaleOriginX + ' ' + scaleOriginY;
}
else
{
containerElem.style.transform = 'none';
}
},
/** @private */
adjustInteractionArea: function(cellElem, isHotCell)
{
var areaElem = this.getInteractionAreaElemOfCell(cellElem);
if (areaElem)
{
if (!isHotCell)
{
cellElem.appendChild(areaElem);
}
else // may adjust position
{
if (cellElem[this.EXCEED_TOPRIGHT_FIELD])
{
var widgetParent = this.getWidgetParentElemOfCell(cellElem);
widgetParent.appendChild(areaElem);
}
}
}
},
/**
* Called when cellWidth or cellHeight property changes.
* @private
*/
updateAllCells: function()
{
var cells = this.getAllCells();
for (var i = 0, l = cells.length; i < l; ++i)
{
this.updateCell(cells[i], cells[i] === this.getHotCell());
}
},
/** @private */
restoreCellShrink: function(cell)
{
var elem = this.getWidgetParentElemOfCell(cell);
if (elem)
{
elem.style.transform = 'none';
}
},
/**
* Notify that the hot cell has been changed.
* @private
*/
hotCellChanged: function(oldCell, newCell)
{
if (oldCell)
{
this.updateCell(oldCell, false);
EU.removeClass(oldCell, CNS.STATE_HOVER);
}
if (newCell)
{
EU.addClass(newCell, CNS.STATE_HOVER);
if (this.getRestoreWidgetSizeOnHotTrack())
{
this.restoreCellShrink(newCell);
this.adjustInteractionArea(newCell, true);
}
}
},
/** @private */
reactCellMouseEnter: function(e)
{
var target = e.getTarget();
this.setHotCell(target);
},
/** @private */
reactCellMouseLeave: function(e)
{
var target = e.getTarget();
if (this.getHotCell() === target)
{
this.setHotCell(null);
}
},
/** @private */
reactCellClick: function(e)
{
var target = e.getTarget();
if (DU.isDescendantOf(target, this.getAddingCell()) || target === this.getAddingCell())
this.createWidget();
},
/** @private */
reactChildWidgetChange: function(e)
{
var widget = e.widget;
if (widget && this.hasChild(widget))
{
this.updateCell(this.getWidgetCell(widget), this.getHotCell() === this.getWidgetCell(widget));
}
}
});
})();