SOURCE CODE: Uize.Widget.Collection.Dynamic
/*______________
| ______ | U I Z E J A V A S C R I P T F R A M E W O R K
| / / | ---------------------------------------------------
| / O / | MODULE : Uize.Widget.Collection.Dynamic Class
| / / / |
| / / / /| | ONLINE : http://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2007-2012 UIZE
| /___ | LICENSE : Available under MIT License or GNU General Public License
|_______________| http://www.uize.com/license.html
*/
/* Module Meta Data
type: Class
importance: 4
codeCompleteness: 80
testCompleteness: 0
docCompleteness: 3
*/
/*?
Introduction
The =Uize.Widget.Collection.Dynamic= class extends =Uize.Widget.Collection= by adding dynamic adding, removing, and drag-and-drop re-ordering of items.
*DEVELOPERS:* `Chris van Rensburg`, `Jan Borgersen`, `Rich Bean`, `Tim Carter`, `Vinson Chuong`
*/
Uize.module ({
name:'Uize.Widget.Collection.Dynamic',
required:[
'Uize.Node',
'Uize.Widget.Drag',
'Uize.Tooltip'
],
builder:function (_superclass) {
/*** Variables for Scruncher Optimization ***/
var
_true = true,
_false = false,
_null = null,
_emptyString = '',
_Uize_Node = Uize.Node,
_Uize_Tooltip = Uize.Tooltip
;
/*** Class Constructor ***/
var
_class = _superclass.subclass (
_null,
function () {
var _this = this;
/*** watch for dragging of items ***/
var
_itemInitiatingDrag,
_itemDisplayOrderNo, // 0 is normal, 1 is reverse
_itemsDragged,
_itemsDraggedLength,
_itemsCoords,
_itemWidgetOver,
_itemWidgetOverCoords,
_insertPointItem,
_insertPointModeNo, // 0 is before, 1 is after
_insertPointCoords,
_lastInsertPointItem,
_lastInsertPointModeNo,
_orientationNo,
_insertionMarkerNode,
_insertionMarkerDims,
_axisPosName,
_axisDimName,
_drag = _this.addChild ('drag',Uize.Widget.Drag,{nodeMap:{'':_null}})
;
function _setInDrag (_inDrag) {
var
_opacity = _inDrag ? _this._itemVestigeOpacity : 1,
_draggingTooltip = _this.getNode ('tooltipDragging')
;
for (var _itemDraggedNo = _itemsDraggedLength; --_itemDraggedNo > -1;)
_itemsDragged [_itemDraggedNo].setNodeOpacity ('',_opacity)
;
_inDrag &&
_Uize_Node.setInnerHtml (
_draggingTooltip,
_this.localize (
'draggingToReorder' + (_itemsDraggedLength > 1 ? 'Plural' : 'Singular'),
{totalItems:_itemsDraggedLength}
)
)
;
_Uize_Tooltip.showTooltip (_draggingTooltip,_inDrag,_true);
}
_drag.wire ({
'Drag Start':
function () {
_itemDisplayOrderNo = _this._itemDisplayOrder == 'reverse' ? 1 : 0;
_itemInitiatingDrag.set ({over:_false});
/*** determine items being dragged ***/
var _itemInitiatingDragIsSelected = _itemInitiatingDrag.get ('selected');
if (!_itemInitiatingDragIsSelected) {
_this.selectAll (_false);
_this._ensureItemDraggedIsSelected && _itemInitiatingDrag.set ({selected:_true});
}
if (!_this._dragIgnoresLocked) {
// if drag obeys the locked property, the locked objects are not draggable.
if (_itemInitiatingDragIsSelected) {
for (
var
_itemsSelectedIdx = -1,
_itemsToDrag = _this.getSelected (),
_itemsToDragLength = _itemsToDrag.length
;
++_itemsSelectedIdx < _itemsToDragLength;
)
_itemsToDrag[_itemsSelectedIdx].get ('locked') &&
_itemsToDrag[_itemsSelectedIdx].set ({selected:_false})
;
// cancel drag if nothing is selected
_this.get ('totalSelected') || _drag.set ({dragCancelled:_true});
}
else if (_itemInitiatingDrag.get ('locked'))
_drag.set ({dragCancelled:_true})
;
}
_itemsDragged =
_itemInitiatingDragIsSelected ? _this.getSelected () : [_itemInitiatingDrag]
;
_itemsDraggedLength = _itemsDragged.length;
/*** capture coords for item widgets (for performance during drag) ***/
_itemsCoords = [];
_this.forAll (
function (_itemWidget) {
_itemsCoords.push (_Uize_Node.getCoords (_itemWidget.getNode ()));
}
);
/*** initialize ***/
var
_itemsCoordsLength = _itemsCoords.length,
_totalItemsMinus1 = _itemsCoordsLength - 1,
_itemsCoords0 = _itemsCoords [_itemDisplayOrderNo ? _totalItemsMinus1 : 0],
_itemsCoords1 = _itemsCoords [_itemDisplayOrderNo ? _totalItemsMinus1 - 1 : 1]
;
_orientationNo =
_totalItemsMinus1 && _itemsCoords1.top > _itemsCoords0.bottom
? 1 /* 1 = items layed out vertically */
: 0 /* 0 = items layed out horizontally */
;
_axisPosName = _orientationNo ? 'top' : 'left';
_axisDimName = _orientationNo ? 'height' : 'width';
_insertPointItem = _insertPointModeNo = _insertPointCoords = _lastInsertPointItem = _lastInsertPointModeNo = _null;
_insertionMarkerNode = _this.getNode ('insertionMarker');
_insertionMarkerDims = _Uize_Node.getDimensions (_insertionMarkerNode);
/*** expand drop coordinates for item widgets (performance optimization) ***/
for (
var
_itemWidgetNo = -1,
_itemWidgetSpacing = _totalItemsMinus1
?
_itemsCoords1 [_axisPosName] -
(_itemsCoords0 [_axisPosName] + _itemsCoords0 [_axisDimName] - 1)
: 0
,
_itemWidgetSpacingDiv2 = _itemWidgetSpacing / 2
;
++_itemWidgetNo < _itemsCoordsLength;
) {
var _itemWidgetCoords = _itemsCoords [_itemWidgetNo];
_itemWidgetCoords [_axisPosName] -= _itemWidgetSpacingDiv2;
_itemWidgetCoords [_axisDimName] += _itemWidgetSpacing;
}
_setInDrag (_true);
},
'Drag Update':
function (_event) {
var
_documentElement = document.documentElement,
_dragEventPos = _drag.eventPos
;
_Uize_Tooltip.positionTooltip (
_this.getNode ('tooltipDragging'),
{pageX:_dragEventPos [0],pageY:_dragEventPos [1]}
);
function _pointWithinCoords (_coords) {
return (
_coords &&
_Uize_Node.doRectanglesOverlap (
_coords.left,_coords.top,_coords.width,_coords.height,
_dragEventPos [0],_dragEventPos [1],1,1
)
);
}
if (!_pointWithinCoords (_itemWidgetOverCoords)) {
_itemWidgetOver = _itemWidgetOverCoords = _null;
_this.forAll (
function (_itemWidget,_itemWidgetNo) {
var _itemWidgetCoords = _itemsCoords [_itemWidgetNo];
if (_pointWithinCoords (_itemWidgetCoords)) {
_itemWidgetOver = _itemWidget;
_itemWidgetOverCoords = _itemWidgetCoords;
}
return !_itemWidgetOver;
}
);
}
if (!_pointWithinCoords (_insertPointCoords)) {
_insertPointItem = _insertPointCoords = _null;
if (_itemWidgetOver && !Uize.isIn (_itemsDragged,_itemWidgetOver)) {
var
_axisDim = _itemWidgetOverCoords [_axisDimName],
_axisDimDiv2 = _axisDim / 2,
_axisLower = _itemWidgetOverCoords [_axisPosName],
_axisCenter = _axisLower + _axisDimDiv2
;
_insertPointItem = _itemWidgetOver;
_insertPointModeNo = _dragEventPos [_orientationNo] < _axisCenter ? 0 : 1;
_insertPointCoords = Uize.clone (_itemWidgetOverCoords);
_insertPointCoords [_axisPosName] = _insertPointModeNo ? _axisCenter : _axisLower;
_insertPointCoords [_axisDimName] = _axisDimDiv2;
}
}
if (
_insertPointItem != _lastInsertPointItem ||
_insertPointModeNo != _lastInsertPointModeNo
) {
_this.displayNode (_insertionMarkerNode,!!_insertPointItem);
if (_insertPointItem) {
var _insertionMarkerCoords = Uize.clone (_insertPointCoords);
_insertionMarkerCoords [_axisPosName] +=
(_insertPointModeNo ? _insertPointCoords [_axisDimName] : 0)
- _insertionMarkerDims [_axisDimName] / 2
;
delete _insertionMarkerCoords [_axisDimName];
_Uize_Node.setCoords (_insertionMarkerNode,_insertionMarkerCoords);
}
_lastInsertPointItem = _insertPointItem;
_lastInsertPointModeNo = _insertPointModeNo;
}
_drag.set ({cursor:_insertPointItem || _itemWidgetOver ? 'move' : 'not-allowed'});
},
'Drag Done':
function (_event) {
if (_drag.get ('dragStarted')) {
_setInDrag (_false);
_this.displayNode ('insertionMarker',_false);
function _finishDrag () {
if (_insertPointItem && !_drag.get ('dragCancelled')) {
var _itemWidgets = _this.itemWidgets;
/*** handle the 'after' insert mode ***/
if (_insertPointModeNo ^ _itemDisplayOrderNo) {
var
_itemWidgetsLength = _itemWidgets.length,
_insertionIndex = Uize.indexIn (_itemWidgets,_insertPointItem) + 1
;
_insertPointItem = _null;
while (_insertionIndex < _itemWidgetsLength) {
var _itemWidget = _itemWidgets [_insertionIndex];
if (!Uize.isIn (_itemsDragged,_itemWidget)) {
_insertPointItem = _itemWidget;
break;
} else {
_insertionIndex++;
}
}
}
/*** perform the move ***/
for (var _itemDraggedNo = -1; ++_itemDraggedNo < _itemsDraggedLength;)
_this.move (_itemsDragged [_itemDraggedNo],_insertPointItem)
;
/*** fire events informing of move ***/
_this.fire ('Items Reordered');
_this._fireItemsChangedEvent ();
}
}
_this._confirmToDrag
? _this.confirm ({
state:'warning',
title:_this.localize ('confirmDragToReorderTitle'),
message:_this.localize ('confirmDragToReorderPrompt'),
yesHandler:function () {
_this._confirmToDrag = _false;
_this.fire ('Drag Confirmed');
_finishDrag ();
},
noHandler:function () {
_drag.set ({dragCancelled:true});
}
})
: _finishDrag ()
;
}
}
});
/*** initiate drag using the drag widget, and let it do the rest ***/
_this.wire (
'Item Mouse Down',
function (_event) {
if (_this._dragToReorder) {
_itemInitiatingDrag = _event.source;
_drag.initiate (_event.domEvent);
}
_event.bubble = _false;
}
);
}
),
_classPrototype = _class.prototype
;
/*** Private Instance Methods ***/
_classPrototype._addItem = function (_widgetProperties) {
var
_this = this,
_propertiesProperty = _widgetProperties.properties,
_itemWidgetName = _this.makeItemWidgetName (_propertiesProperty)
;
_this.get ('items').push (_propertiesProperty);
return _this.addItemWidget (_itemWidgetName,_widgetProperties);
};
_classPrototype._fireItemsChangedEvent = function () {this.fire ('Items Changed')};
/*** Public Instance Methods ***/
var _selectedProperty = {selected:_true};
_classPrototype.add = function (_itemsToAdd) {
var
_this = this,
_itemWidgetsAdded = []
;
if (!Uize.isArray (_itemsToAdd)) _itemsToAdd = [_itemsToAdd];
var _itemsToAddLength = _itemsToAdd.length;
if (_itemsToAddLength) {
_this._makeNewlyAddedSelected && _this.selectAll (_false);
var _commonProperties = _this._makeNewlyAddedSelected ? _selectedProperty : _null;
for (var _itemToAddNo = -1; ++_itemToAddNo < _itemsToAddLength;)
_itemWidgetsAdded.push (
_this._addItem (Uize.copyInto (_itemsToAdd [_itemToAddNo],_commonProperties))
)
;
}
_this._fireItemsChangedEvent ();
return _itemWidgetsAdded;
};
_classPrototype.getItemWidgetProperties = function () {
var _this = this;
return (
Uize.copyInto (
{
previewTooltip:
function () {return _this._dragToReorder ? _this.getNode ('tooltipDragToReorder') : _null}
},
_this.get ('itemWidgetProperties')
)
);
};
_classPrototype.move = function (_itemWidgetToMove, _insertionPointItem) {
var
_this = this,
_insertAfter = _this._itemDisplayOrder == 'reverse',
_insertionPointNode = _insertionPointItem ? _insertionPointItem.getNode () : _null,
_items = _this.get ('items'),
_itemWidgets = _this.itemWidgets,
_rootNode = _this.getNode ('items'),
_node = _itemWidgetToMove.getNode (),
_nodeToInsertBefore = _insertAfter
? (_insertionPointNode ? _insertionPointNode.nextSibling : _rootNode.childNodes[0])
: _insertionPointNode
;
// reorder the DOM element
_nodeToInsertBefore ? _rootNode.insertBefore (_node, _nodeToInsertBefore) : _rootNode.appendChild(_node);
/*** reorder itemWidget in the itemWidgets, and item in items ***/
/*** splice out item being dragged ***/
var
_spliceOutPos = Uize.indexIn (_itemWidgets,_itemWidgetToMove),
_item = _items [_spliceOutPos]
;
_itemWidgets.splice (_spliceOutPos,1);
_items.splice (_spliceOutPos,1);
/*** splice item into new position ***/
var _spliceInPos = _insertionPointItem
? Uize.indexIn (_itemWidgets,_insertionPointItem)
: _itemWidgets.length
;
_itemWidgets.splice (_spliceInPos,0,_itemWidgetToMove);
_items.splice (_spliceInPos,0,_item);
};
_classPrototype.processItemTemplate = function (_templateNode) {
// NOTE: This code is pretty much identical to the code in buildHtml (of Uize.Widget), but there's no
// easy way to get the template into the markup so that it can do what it does.
var _nodeInnerHtml = _templateNode.innerHTML;
return Uize.Template &&_templateNode.tagName == 'SCRIPT' && _templateNode.type == 'text/jst'
? Uize.Template.compile (_nodeInnerHtml, {openerToken:'[%',closerToken:'%]'})
: function (_input) {return _nodeInnerHtml.replace (/ITEMWIDGETNAME/g, _input.name)}
;
};
_classPrototype.wireUi = function () {
var _this = this;
if (!_this.isWired) {
var
_docBody = document.body,
_insertionMarkerNode = _this.getNode ('insertionMarker'),
_itemWidgetProperties = {},
_itemTemplateNode = _this.getNode ('itemTemplate')
;
// Pull insertion marker to root
if (_insertionMarkerNode && _insertionMarkerNode.parentNode != _docBody) {
_docBody.insertBefore (_insertionMarkerNode, _docBody.childNodes[0]);
_this.setNodeStyle (
_insertionMarkerNode,
{
display:'none',
position:'absolute',
zIndex:10000,
left:_emptyString,
top:_emptyString,
right:_emptyString,
bottom:_emptyString
}
);
}
if (_itemTemplateNode)
_itemWidgetProperties.html = _this.processItemTemplate (_itemTemplateNode)
;
_itemWidgetProperties.built = _false;
_itemWidgetProperties.container = _this.getNode ('items');
_itemWidgetProperties.insertionMode = _this._itemDisplayOrder == 'reverse' ? 'inner top' : 'inner bottom';
// Update the already created item widgets if the UI hasn't been built yet
_this.get('built')
|| _this.forAll( function(_itemWidget) { _itemWidget.set(_itemWidgetProperties) } );
// For future creation of item widgets we need to update the item widget properties to have all the UI building stuff
_this.set({itemWidgetProperties:Uize.copyInto(_itemWidgetProperties, _this.get('itemWidgetProperties') || {})});
_superclass.prototype.wireUi.call (_this);
}
};
/*** Register Properties ***/
_class.registerProperties ({
_confirmToDrag:{
name:'confirmToDrag',
value:_false
},
_dragIgnoresLocked:{
name:'dragIgnoresLocked',
value:_true
/*
If true, then drag will drag locked CollectionItem widgets. If false, then drag will de-select any locked CollectionItem widgets prior to carrying out the drag.
*/
},
_dragToReorder:{
name:'dragToReorder',
value:_false
},
_ensureItemDraggedIsSelected:{
name:'ensureItemDraggedIsSelected',
value:_false
/*
If true, an unselected item that is dragged will be selected. If false, an unselected item that is dragged will remain unselected.
*/
},
_itemDisplayOrder:{
name:'itemDisplayOrder',
value:'normal' // normal | reverse
},
_makeNewlyAddedSelected:{
name:'makeNewlyAddedSelected',
value:_true
},
_itemVestigeOpacity:{
name:'itemVestigeOpacity',
value:.2
}
});
return _class;
}
});