2 Copyright 2007 - 2008 University of Toronto
\r
3 Copyright 2007 University of Cambridge
\r
5 Licensed under the Educational Community License (ECL), Version 2.0 or the New
\r
6 BSD license. You may not use this file except in compliance with one these
\r
9 You may obtain a copy of the ECL 2.0 License and BSD License at
\r
10 https://source.fluidproject.org/svn/LICENSE.txt
\r
13 // Declare dependencies.
\r
14 var fluid = fluid || {};
\r
16 fluid.Reorderer = function (container, params) {
\r
19 var thisReorderer = this;
\r
20 var theAvatar = null;
\r
22 this.domNode = jQuery (container);
\r
24 // the reorderable DOM element that is currently active
\r
25 this.activeItem = null;
\r
27 this.layoutHandler = null;
\r
29 this.messageNamebase = "message-bundle:";
\r
31 this.orderableFinder = null;
\r
34 defaultStyle: "orderable-default",
\r
35 selected: "orderable-selected",
\r
36 dragging: "orderable-dragging",
\r
37 hover: "orderable-hover",
\r
38 focusTarget: "orderable-focus-target",
\r
39 dropMarker: "orderable-drop-marker",
\r
40 avatar: "orderable-avatar"
\r
44 fluid.mixin (this, params);
\r
48 * Return the element within the item that should receive focus. This is determined by
\r
49 * cssClasses.focusTarget. If it is not specified, the item itself is returned.
\r
51 * @param {Object} item
\r
52 * @return {Object} The element that should receive focus in the specified item.
\r
54 this.findElementToFocus = function (item) {
\r
55 var elementToFocus = jQuery ("." + this.cssClasses.focusTarget, item).get (0);
\r
56 if (elementToFocus) {
\r
57 return elementToFocus;
\r
62 function setupDomNode (domNode) {
\r
63 domNode.focus(thisReorderer.handleFocus);
\r
64 domNode.blur (thisReorderer.handleBlur);
\r
65 domNode.keydown (thisReorderer.handleKeyDown);
\r
66 domNode.keyup (thisReorderer.handleKeyUp);
\r
67 domNode.removeAttr ("aaa:activedescendent");
\r
71 * Changes the current focus to the specified item.
\r
72 * @param {Object} anItem
\r
74 this.focusItem = function(anItem) {
\r
75 if (this.activeItem !== anItem) {
\r
76 this.changeActiveItemToDefaultState();
\r
77 this._setActiveItem (anItem);
\r
80 var jActiveItem = jQuery (this.activeItem);
\r
81 jActiveItem.removeClass (this.cssClasses.defaultStyle);
\r
82 jActiveItem.addClass (this.cssClasses.selected);
\r
84 this.addFocusToElement (this.findElementToFocus (this.activeItem));
\r
87 this.addFocusToElement = function (anElement) {
\r
88 var jElement = jQuery (anElement);
\r
89 if (!jElement.hasTabIndex ()) {
\r
90 jElement.tabIndex (-1);
\r
95 this.removeFocusFromElement = function (anElement) {
\r
96 jQuery (anElement).blur ();
\r
100 * Changes focus to the active item.
\r
102 this.selectActiveItem = function() {
\r
103 if (!this.activeItem) {
\r
104 var orderables = this.orderableFinder (this.domNode.get(0));
\r
105 if (orderables.length > 0) {
\r
106 this._setActiveItem (orderables[0]);
\r
112 this.focusItem (this.activeItem);
\r
115 this.handleFocus = function (evt) {
\r
116 thisReorderer.selectActiveItem();
\r
120 this.handleBlur = function (evt) {
\r
121 // Temporarily disabled blur handling in IE. See FLUID-7 for details.
\r
122 if (!jQuery.browser.msie) {
\r
123 thisReorderer.changeActiveItemToDefaultState();
\r
127 this.changeActiveItemToDefaultState = function() {
\r
128 if (this.activeItem) {
\r
129 var jActiveItem = jQuery (this.activeItem);
\r
130 jActiveItem.removeClass (this.cssClasses.selected);
\r
131 jActiveItem.addClass (this.cssClasses.defaultStyle);
\r
132 this.removeFocusFromElement (jActiveItem);
\r
136 this.handleKeyDown = function (evt) {
\r
137 if (thisReorderer.activeItem && evt.keyCode === fluid.keys.CTRL) {
\r
138 jQuery (thisReorderer.activeItem).removeClass (thisReorderer.cssClasses.selected);
\r
139 jQuery (thisReorderer.activeItem).addClass (thisReorderer.cssClasses.dragging);
\r
140 thisReorderer.activeItem.setAttribute ("aaa:grab", "true");
\r
144 return thisReorderer.handleArrowKeyPress(evt);
\r
147 this.handleKeyUp = function (evt) {
\r
148 if (thisReorderer.activeItem && evt.keyCode === fluid.keys.CTRL) {
\r
149 jQuery (thisReorderer.activeItem).removeClass (thisReorderer.cssClasses.dragging);
\r
150 jQuery (thisReorderer.activeItem).addClass (thisReorderer.cssClasses.selected);
\r
151 thisReorderer.activeItem.setAttribute ("aaa:grab", "supported");
\r
156 this.handleArrowKeyPress = function (evt) {
\r
157 if (thisReorderer.activeItem) {
\r
158 switch (evt.keyCode) {
\r
159 case fluid.keys.DOWN:
\r
160 evt.preventDefault();
\r
161 thisReorderer.handleDownArrow (evt.ctrlKey);
\r
163 case fluid.keys.UP:
\r
164 evt.preventDefault();
\r
165 thisReorderer.handleUpArrow (evt.ctrlKey);
\r
167 case fluid.keys.LEFT:
\r
168 evt.preventDefault();
\r
169 thisReorderer.handleLeftArrow (evt.ctrlKey);
\r
171 case fluid.keys.RIGHT:
\r
172 evt.preventDefault();
\r
173 thisReorderer.handleRightArrow (evt.ctrlKey);
\r
181 this.handleUpArrow = function (isCtrl) {
\r
183 this.layoutHandler.moveItemUp (this.activeItem);
\r
184 this.findElementToFocus (this.activeItem).focus();
\r
186 this.focusItem (this.layoutHandler.getItemAbove(this.activeItem));
\r
190 this.handleDownArrow = function (isCtrl) {
\r
192 this.layoutHandler.moveItemDown (this.activeItem);
\r
193 this.findElementToFocus (this.activeItem).focus();
\r
195 this.focusItem (this.layoutHandler.getItemBelow (this.activeItem));
\r
199 this.handleRightArrow = function (isCtrl) {
\r
201 this.layoutHandler.moveItemRight (this.activeItem);
\r
202 jQuery(this.findElementToFocus (this.activeItem)).focus();
\r
204 this.focusItem (this.layoutHandler.getRightSibling (this.activeItem));
\r
208 this.handleLeftArrow = function (isCtrl) {
\r
210 this.layoutHandler.moveItemLeft (this.activeItem);
\r
211 this.findElementToFocus (this.activeItem).focus();
\r
213 this.focusItem (this.layoutHandler.getLeftSibling (this.activeItem));
\r
217 this._fetchMessage = function (messagekey) {
\r
218 var messageID = this.messageNamebase + messagekey;
\r
219 var node = document.getElementById (messageID);
\r
221 return node? node.innerHTML: "[Message not found at id " + messageID + "]";
\r
224 this._setActiveItem = function (anItem) {
\r
225 this.activeItem = anItem;
\r
226 this._updateActiveDescendent();
\r
229 this._updateActiveDescendent = function() {
\r
230 if (this.activeItem) {
\r
231 this.domNode.attr ("aaa:activedescendent", this.activeItem.id);
\r
233 this.domNode.removeAttr ("aaa:activedescendent");
\r
237 var dropMarker; // private scratch variable
\r
240 * evt.data - the droppable DOM element.
\r
242 function trackMouseMovement (evt) {
\r
243 if (thisReorderer.layoutHandler.isMouseBefore (evt, evt.data)) {
\r
244 jQuery (evt.data).before (dropMarker);
\r
247 jQuery (evt.data).after (dropMarker);
\r
252 * Given an item, make it draggable.
\r
254 function setUpDraggable (anItem) {
\r
255 anItem.draggable ({
\r
256 helper: function() {
\r
257 theAvatar = jQuery (this).clone();
\r
258 jQuery (theAvatar).removeAttr ("id");
\r
259 jQuery ("[id]", theAvatar).removeAttr ("id");
\r
260 jQuery (":hidden", theAvatar).remove();
\r
261 jQuery ("input", theAvatar).attr ("disabled", "true");
\r
262 theAvatar.addClass (thisReorderer.cssClasses.avatar);
\r
265 start: function (e, ui) {
\r
266 thisReorderer.focusItem (ui.draggable.element);
\r
267 jQuery (ui.draggable.element).addClass (thisReorderer.cssClasses.dragging);
\r
268 ui.draggable.element.setAttribute ("aaa:grab", "true");
\r
270 // In order to create valid html, the drop marker is the same type as the node being dragged.
\r
271 // This creates a confusing UI in cases such as an ordered list.
\r
272 // drop marker functionality should be made pluggable.
\r
273 dropMarker = document.createElement (ui.draggable.element.tagName);
\r
274 jQuery (dropMarker).addClass (thisReorderer.cssClasses.dropMarker);
\r
275 dropMarker.style.visibility = "hidden";
\r
277 stop: function(e, ui) {
\r
278 jQuery (ui.draggable.element).removeClass (thisReorderer.cssClasses.dragging);
\r
279 thisReorderer.activeItem.setAttribute ("aaa:grab", "supported");
\r
280 thisReorderer.domNode.focus();
\r
281 if (dropMarker.parentNode) {
\r
282 dropMarker.parentNode.removeChild (dropMarker);
\r
288 } // end setUpDraggable()
\r
291 * Make item a drop target.
\r
293 function setUpDroppable (anItem, selector) {
\r
294 anItem.droppable ({
\r
296 tolerance: "pointer",
\r
297 over: function (e, ui) {
\r
298 // the second parameter to bind() can be accessed through the event as event.data
\r
299 jQuery (ui.droppable.element).bind ("mousemove", ui.droppable.element, trackMouseMovement);
\r
300 jQuery (theAvatar).bind ("mousemove", ui.droppable.element, trackMouseMovement);
\r
301 dropMarker.style.visibility = "visible";
\r
303 out: function (e, ui) {
\r
304 dropMarker.style.visibility = "hidden";
\r
305 jQuery (ui.droppable.element).unbind ("mousemove", trackMouseMovement);
\r
306 jQuery (theAvatar).unbind ("mousemove", trackMouseMovement);
\r
308 drop: function (e, ui) {
\r
309 thisReorderer.layoutHandler.mouseMoveItem(e, ui.draggable.element, ui.droppable.element);
\r
310 jQuery (ui.droppable.element).unbind ("mousemove", trackMouseMovement);
\r
311 jQuery (theAvatar).unbind ("mousemove", trackMouseMovement);
\r
315 } // end setUpDroppable().
\r
318 * Given an item, make it a draggable and a droppable with the relevant properties and functions.
\r
319 * @param anItem The element to make draggable and droppable.
\r
320 * @param selector The jQuery selector(s) that select all the orderables.
\r
322 function setUpDnDItem (anItem, selector) {
\r
323 anItem.mouseover (
\r
325 jQuery (this).addClass (thisReorderer.cssClasses.hover);
\r
331 jQuery (this).removeClass (thisReorderer.cssClasses.hover);
\r
335 // Make anItem draggable and a drop target (in the jQuery UI sense).
\r
336 setUpDraggable (anItem);
\r
337 setUpDroppable (anItem, selector);
\r
339 } // end setUpDnDItem()
\r
341 function initOrderables() {
\r
342 var items = thisReorderer.orderableFinder (thisReorderer.domNode.get(0));
\r
343 if (items.length === 0) {
\r
347 // Create a selector based on the ids of the nodes for use with drag and drop.
\r
348 // This should be replaced with using the actual nodes rather then a selector
\r
349 // but will require a patch to jquery's DnD.
\r
350 // See: FLUID-71, FLUID-112
\r
352 for (var i = 0; i < items.length; i++) {
\r
353 var item = items[i];
\r
354 selector += "[id=" + item.id + "]";
\r
355 if (i !== items.length - 1) {
\r
360 // Setup orderable item including drag and drop.
\r
361 for (i = 0; i < items.length; i++) {
\r
362 jQuery (items[i]).addClass (thisReorderer.cssClasses.defaultStyle);
\r
363 setUpDnDItem (jQuery (items[i]), selector);
\r
366 // Add any other drop targets (e.g., any unmoveable ones).
\r
367 if ((thisReorderer.droppableFinder) && (thisReorderer.droppableFinder.constructor === Function)) {
\r
368 var extraDropTargets = thisReorderer.droppableFinder (thisReorderer.domNode);
\r
369 for (i = 0; i < extraDropTargets.length; i++) {
\r
370 var jExtraDropTarget = jQuery (extraDropTargets[i]);
\r
371 if (!jExtraDropTarget.droppableInstance()) {
\r
372 setUpDroppable (jExtraDropTarget, selector);
\r
376 } // end initOrderables().
\r
379 * Finds the "orderable" parent element given a child element.
\r
381 this._findReorderableParent = function (childElement, items) {
\r
382 if (!childElement) {
\r
386 for (var i=0; i<items.length; i++) {
\r
387 if (childElement === items[i]) {
\r
388 return childElement;
\r
391 return this._findReorderableParent (childElement.parentNode, items);
\r
395 // Final initialization of the Reorderer at the end of the construction process
\r
396 if (this.domNode) {
\r
397 setupDomNode(this.domNode);
\r
400 }; // End Reorderer
\r
402 /*******************
\r
403 * Layout Handlers *
\r
404 *******************/
\r
406 // Shared private functions.
\r
407 var moveItem = function (item, relatedItemInfo, defaultPlacement, wrappedPlacement) {
\r
408 var itemPlacement = defaultPlacement;
\r
409 if (relatedItemInfo.hasWrapped) {
\r
410 itemPlacement = wrappedPlacement;
\r
413 // The "after" and "before" strings are an artifact of using dojo.place.
\r
414 // This should be refactored.
\r
415 if (itemPlacement === "after") {
\r
416 jQuery (relatedItemInfo.item).after (item);
\r
418 jQuery (relatedItemInfo.item).before (item);
\r
423 * For drag-and-drop during the drag: is the mouse over the "before" half
\r
424 * of the droppable? In the case of a vertically oriented set of orderables,
\r
425 * "before" means "above". For a horizontally oriented set, "before" means
\r
428 var isMouseBefore = function (evt, droppableEl, orientation) {
\r
430 if (orientation === fluid.orientation.VERTICAL) {
\r
431 mid = jQuery (droppableEl).offset().top + (droppableEl.offsetHeight / 2);
\r
432 return (evt.pageY < mid);
\r
434 mid = jQuery (droppableEl).offset().left + (droppableEl.offsetWidth / 2);
\r
435 return (evt.clientX < mid);
\r
439 var itemInfoFinders = {
\r
441 * A general get{Left|Right}SiblingInfo() given an item, a list of orderables and a direction.
\r
442 * The direction is encoded by either a +1 to move right, or a -1 to
\r
443 * move left, and that value is used internally as an increment or
\r
444 * decrement, respectively, of the index of the given item.
\r
446 getSiblingInfo: function (item, orderables, /* +1, -1 */ incDecrement) {
\r
447 var index = jQuery (orderables).index (item) + incDecrement;
\r
448 var hasWrapped = false;
\r
450 // Handle wrapping to 'before' the beginning.
\r
451 if (index === -1) {
\r
452 index = orderables.length - 1;
\r
455 // Handle wrapping to 'after' the end.
\r
456 else if (index === orderables.length) {
\r
460 // Handle case where the passed-in item is *not* an "orderable"
\r
461 // (or other undefined error).
\r
463 else if (index < -1 || index > orderables.length) {
\r
467 return {item: orderables[index], hasWrapped: hasWrapped};
\r
471 * Returns an object containing the item that is to the right of the given item
\r
472 * and a flag indicating whether or not the process has 'wrapped' around the end of
\r
473 * the row that the given item is in
\r
475 getRightSiblingInfo: function (item, orderables) {
\r
476 return this.getSiblingInfo (item, orderables, 1);
\r
480 * Returns an object containing the item that is to the left of the given item
\r
481 * and a flag indicating whether or not the process has 'wrapped' around the end of
\r
482 * the row that the given item is in
\r
484 getLeftSiblingInfo: function (item, orderables) {
\r
485 return this.getSiblingInfo (item, orderables, -1);
\r
489 * Returns an object containing the item that is below the given item in the current grid
\r
490 * and a flag indicating whether or not the process has 'wrapped' around the end of
\r
491 * the column that the given item is in. The flag is necessary because when an image is being
\r
492 * moved to the resulting item location, the decision of whether or not to insert before or
\r
493 * after the item changes if the process wrapped around the column.
\r
495 getItemInfoBelow: function (inItem, orderables) {
\r
496 var curCoords = jQuery (inItem).offset();
\r
498 var firstItemInColumn, currentItem;
\r
500 for (i = 0; i < orderables.length; i++) {
\r
501 currentItem = orderables [i];
\r
502 iCoords = jQuery (orderables[i]).offset();
\r
503 if (iCoords.left === curCoords.left) {
\r
504 firstItemInColumn = firstItemInColumn || currentItem;
\r
505 if (iCoords.top > curCoords.top) {
\r
506 return {item: currentItem, hasWrapped: false};
\r
511 firstItemInColumn = firstItemInColumn || orderables [0];
\r
512 return {item: firstItemInColumn, hasWrapped: true};
\r
516 * Returns an object containing the item that is above the given item in the current grid
\r
517 * and a flag indicating whether or not the process has 'wrapped' around the end of
\r
518 * the column that the given item is in. The flag is necessary because when an image is being
\r
519 * moved to the resulting item location, the decision of whether or not to insert before or
\r
520 * after the item changes if the process wrapped around the column.
\r
522 getItemInfoAbove: function (inItem, orderables) {
\r
523 var curCoords = jQuery (inItem).offset();
\r
525 var lastItemInColumn, currentItem;
\r
527 for (i = orderables.length - 1; i > -1; i--) {
\r
528 currentItem = orderables [i];
\r
529 iCoords = jQuery (orderables[i]).offset();
\r
530 if (iCoords.left === curCoords.left) {
\r
531 lastItemInColumn = lastItemInColumn || currentItem;
\r
532 if (curCoords.top > iCoords.top) {
\r
533 return {item: currentItem, hasWrapped: false};
\r
538 lastItemInColumn = lastItemInColumn || orderables [0];
\r
539 return {item: lastItemInColumn, hasWrapped: true};
\r
544 // Public layout handlers.
\r
545 fluid.ListLayoutHandler = function (params) {
\r
546 var orderableFinder = params.orderableFinder;
\r
547 var container = params.container;
\r
548 var orderChangedCallback = params.orderChangedCallback;
\r
549 var orientation = params.orientation;
\r
551 // Unwrap the container if it's a jQuery.
\r
552 container = (container.jquery) ? container.get(0) : container;
\r
554 orderChangedCallback = orderChangedCallback || function () {};
\r
556 orientation = orientation || fluid.orientation.VERTICAL; // default
\r
558 this.getRightSibling = function (item) {
\r
559 return itemInfoFinders.getRightSiblingInfo(item, orderableFinder(container)).item;
\r
562 this.moveItemRight = function (item) {
\r
563 moveItem (item, itemInfoFinders.getRightSiblingInfo(item, orderableFinder(container)), "after", "before");
\r
564 orderChangedCallback();
\r
567 this.getLeftSibling = function (item) {
\r
568 return itemInfoFinders.getLeftSiblingInfo(item, orderableFinder(container)).item;
\r
571 this.moveItemLeft = function (item) {
\r
572 moveItem (item, itemInfoFinders.getLeftSiblingInfo(item, orderableFinder(container)), "before", "after");
\r
573 orderChangedCallback();
\r
576 this.getItemBelow = this.getRightSibling;
\r
578 this.getItemAbove = this.getLeftSibling;
\r
580 this.moveItemUp = this.moveItemLeft;
\r
582 this.moveItemDown = this.moveItemRight;
\r
584 this.isMouseBefore = function(evt, droppableEl) {
\r
585 return isMouseBefore(evt, droppableEl, orientation);
\r
588 this.mouseMoveItem = function (e, item, relatedItem) {
\r
589 if (this.isMouseBefore (e, relatedItem)) {
\r
590 jQuery (relatedItem).before (item);
\r
592 jQuery (relatedItem).after (item);
\r
594 orderChangedCallback();
\r
596 }; // End ListLayoutHandler
\r
599 * Items in the Lightbox are stored in a list, but they are visually presented as a grid that
\r
600 * changes dimensions when the window changes size. As a result, when the user presses the up or
\r
601 * down arrow key, what lies above or below depends on the current window size.
\r
603 * The GridLayoutHandler is responsible for handling changes to this virtual 'grid' of items
\r
604 * in the window, and of informing the Lightbox of which items surround a given item.
\r
606 fluid.GridLayoutHandler = function (params) {
\r
607 fluid.ListLayoutHandler.call (this, params);
\r
609 var orderableFinder = params.orderableFinder;
\r
610 var container = params.container;
\r
611 var orderChangedCallback = params.orderChangedCallback;
\r
612 var orientation = fluid.orientation.HORIZONTAL;
\r
614 // Unwrap the container if it's a jQuery.
\r
615 container = (container.jquery) ? container.get(0) : container;
\r
617 orderChangedCallback = orderChangedCallback || function () {};
\r
619 this.getItemBelow = function(item) {
\r
620 return itemInfoFinders.getItemInfoBelow (item, orderableFinder(container)).item;
\r
623 this.moveItemDown = function (item) {
\r
624 moveItem (item, itemInfoFinders.getItemInfoBelow (item, orderableFinder(container)), "after", "before");
\r
625 orderChangedCallback();
\r
628 this.getItemAbove = function (item) {
\r
629 return itemInfoFinders.getItemInfoAbove (item, orderableFinder(container)).item;
\r
632 this.moveItemUp = function (item) {
\r
633 moveItem (item, itemInfoFinders.getItemInfoAbove (item, orderableFinder(container)), "before", "after");
\r
634 orderChangedCallback();
\r
637 // We need to override ListLayoutHandler.isMouseBefore to ensure that the local private
\r
638 // orientation is used.
\r
639 this.isMouseBefore = function(evt, droppableEl) {
\r
640 return isMouseBefore(evt, droppableEl, orientation);
\r
643 }; // End of GridLayoutHandler
\r
645 fluid.portletPerms = fluid.portletPerms || {};
\r
646 fluid.portletPerms.canDrop = function (perms, indexOfItem, indexOfTarget, position) {
\r
647 return (!!perms[indexOfItem][indexOfTarget][position]);
\r
651 * PortletLayout helper object.
\r
653 fluid.PortletLayout = function() {
\r
656 * Calculate the location of the item and the column in which it resides.
\r
657 * @return An object with column index and item index (within that column) properties.
\r
658 * These indices are -1 if the item does not exist in the grid.
\r
660 this.calcColumnAndItemIndex = function (item, portletLayoutJSON) {
\r
661 var indices = { columnIndex: -1, itemIndex: -1 };
\r
662 for (var col = 0; col < portletLayoutJSON.columns.length; col++) {
\r
663 var itemIndex = jQuery (portletLayoutJSON.columns[col].children).index (item.id);
\r
664 if (itemIndex !== -1) {
\r
665 indices.columnIndex = col;
\r
666 indices.itemIndex = itemIndex;
\r
674 * Return the first orderable item in the given column.
\r
676 this.findFirstOrderableSiblingInColumn = function (columnIndex, orderableItems, portletLayoutJSON) {
\r
677 // Pull out the portlet id of the top-most sibling in the column.
\r
678 var topMostOrderableSibling = null;
\r
680 var column = portletLayoutJSON.columns[columnIndex];
\r
682 var id = column.children[itemIndex];
\r
683 topMostOrderableSibling = jQuery ("#" + id)[0];
\r
684 // loop down the column looking for first orderable portlet (i.e. skip over non-movable portlets)
\r
685 while (orderableItems.index (topMostOrderableSibling) === -1) {
\r
687 id = column.children[itemIndex];
\r
688 topMostOrderableSibling = jQuery ("#" + id).get (0);
\r
691 return topMostOrderableSibling;
\r
695 * Return the number of items in the given column. If the column index
\r
696 * is out of bounds, this returns -1.
\r
698 this.numItemsInColumn = function (columnIndex, portletLayoutJSON) {
\r
699 if ((columnIndex < 0) || (columnIndex > portletLayoutJSON.columns.length)) {
\r
703 return portletLayoutJSON.columns[columnIndex].children.length;
\r
707 this.numColumns = function (layout) {
\r
708 return layout.columns.length;
\r
711 this.findLinearIndex = function (itemId, layout) {
\r
712 var columns = layout.columns;
\r
713 var linearIndex = 0;
\r
715 for (var i = 0; i < columns.length; i++) {
\r
716 var idsInCol = columns[i].children;
\r
717 for (var j = 0; j < idsInCol.length; j++) {
\r
718 if (idsInCol[j] === itemId) {
\r
719 return linearIndex;
\r
729 * Move an item within the portletLayoutJSON object.
\r
731 this.updateLayout = function (item, relativeItem, placement, portletLayoutJSON) {
\r
732 if (!item || !relativeItem) { return; }
\r
733 var itemIndices = this.calcColumnAndItemIndex (item, portletLayoutJSON);
\r
735 var itemId = portletLayoutJSON.columns[itemIndices.columnIndex].children[itemIndices.itemIndex];
\r
736 portletLayoutJSON.columns[itemIndices.columnIndex].children.splice (itemIndices.itemIndex, 1);
\r
738 var relativeItemIndices = this.calcColumnAndItemIndex (relativeItem, portletLayoutJSON);
\r
739 var targetCol = portletLayoutJSON.columns[relativeItemIndices.columnIndex].children;
\r
740 targetCol.splice (relativeItemIndices.itemIndex + (placement === "before"? 0 : 1), 0, itemId);
\r
742 }; // End of PortletLayout.
\r
745 * Portlet Layout Handler for reordering portlet-type markup.
\r
747 * General movement guidelines:
\r
749 * - Arrowing sideways will always go to the top (moveable) portlet in the column
\r
750 * - Moving sideways will always move to the top available drop target in the column
\r
751 * - Wrapping is not necessary at this first pass, but is ok
\r
753 fluid.PortletLayoutHandler = function (params) {
\r
754 var orderableFinder = params.orderableFinder;
\r
755 var container = params.container;
\r
756 var orderChangedCallback = params.orderChangedCallback;
\r
757 var portletLayoutJSON = params.portletLayout;
\r
758 var targetPerms = params.dropTargetPermissions;
\r
759 var orientation = fluid.orientation.VERTICAL;
\r
761 var portletLayout = new fluid.PortletLayout();
\r
763 // Unwrap the container if it's a jQuery.
\r
764 container = (container.jquery) ? container.get(0) : container;
\r
766 orderChangedCallback = orderChangedCallback || function () {};
\r
768 // Private Methods.
\r
770 * A general get{Above|Below}Sibling() given an item and a direction.
\r
771 * The direction is encoded by either a +1 to move down, or a -1 to
\r
772 * move up, and that value is used internally as an increment or
\r
773 * decrement, respectively, of the index of the given item.
\r
774 * This implementation does not wrap around.
\r
776 var getVerticalSibling = function (item, /* +1, -1 */ incDecrement) {
\r
777 var orderables = orderableFinder (container);
\r
778 var index = jQuery(orderables).index(item) + incDecrement;
\r
780 // If we wrap, backup
\r
781 if ((index === -1) || (index === orderables.length)) {
\r
784 // Handle case where the passed-in item is *not* an "orderable"
\r
785 // (or other undefined error).
\r
787 else if (index < -1 || index > orderables.length) {
\r
791 return orderables[index];
\r
795 * A general get{Beside}SiblingInfo() given an item and a direction.
\r
796 * The direction is encoded by either a +1 to move right, or a -1 to
\r
798 * Currently, the horizontal sibling defaults to the top orderable item in the
\r
799 * neighboring column.
\r
801 var getHorizontalSibling = function (item, /* +1, -1 */ incDecrement) {
\r
802 var orderables = orderableFinder (container);
\r
804 // go through all the children and find which column the passed in item is located in.
\r
805 // Save that column if found.
\r
806 var colIndex = portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON).columnIndex;
\r
807 if (colIndex === -1) {
\r
810 var sibling = portletLayout.findFirstOrderableSiblingInColumn (colIndex + incDecrement, orderables, portletLayoutJSON);
\r
815 // These methods are public only for our unit tests. They need to be refactored into a portletlayout object.
\r
816 var isFirstInColumn = function (item) {
\r
817 var position = portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON);
\r
818 return (position.itemIndex === 0) ? true : false;
\r
821 var isLastInColumn = function (item) {
\r
822 var position = portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON);
\r
823 return (position.itemIndex === portletLayout.numItemsInColumn (position.columnIndex, portletLayoutJSON)-1) ? true : false;
\r
826 var isInLeftmostColumn = function (item) {
\r
827 if (portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON).columnIndex === 0) {
\r
834 var isInRightmostColumn = function (item) {
\r
835 if (portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON).columnIndex === portletLayout.numColumns (portletLayoutJSON) - 1) {
\r
842 var canDrop = function (itemId, relatedItemId, position) {
\r
843 return fluid.portletPerms.canDrop(
\r
845 portletLayout.findLinearIndex(itemId, portletLayoutJSON),
\r
846 portletLayout.findLinearIndex(relatedItemId, portletLayoutJSON),
\r
850 var moveBefore = function (item, relatedItem) {
\r
851 if (!canDrop (item.id, relatedItem.id, fluid.position.BEFORE)) {
\r
855 jQuery (relatedItem).before (item);
\r
856 portletLayout.updateLayout (item, relatedItem, "before", portletLayoutJSON);
\r
857 portletLayoutJSON = orderChangedCallback() || portletLayoutJSON;
\r
860 var moveAfter = function (item, relatedItem) {
\r
861 if (!canDrop (item.id, relatedItem.id, fluid.position.AFTER)) {
\r
865 jQuery (relatedItem).after (item);
\r
866 portletLayout.updateLayout (item, relatedItem, "after", portletLayoutJSON);
\r
867 portletLayoutJSON = orderChangedCallback() || portletLayoutJSON;
\r
872 this.getRightSibling = function (item) {
\r
873 if (isInRightmostColumn(item)) {
\r
876 return getHorizontalSibling(item, 1);
\r
880 this.moveItemRight = function (item) {
\r
881 var rightSibling = this.getRightSibling (item);
\r
882 jQuery (rightSibling).before (item);
\r
883 portletLayout.updateLayout (item, rightSibling, "before", portletLayoutJSON);
\r
884 // first, stringify the json before sending it
\r
885 portletLayoutJSON = orderChangedCallback() || portletLayoutJSON;
\r
886 // the return value will actually be a big json object with two parts
\r
887 // we'll have to parse it, and separate the two parts
\r
890 this.getLeftSibling = function (item) {
\r
891 if (isInLeftmostColumn(item)) {
\r
894 return getHorizontalSibling(item, -1);
\r
898 this.moveItemLeft = function (item) {
\r
899 var leftSibling = this.getLeftSibling(item);
\r
900 jQuery (leftSibling).before (item);
\r
901 portletLayout.updateLayout (item, leftSibling, "before", portletLayoutJSON);
\r
902 portletLayoutJSON = orderChangedCallback() || portletLayoutJSON;
\r
905 this.getItemAbove = function (item) {
\r
906 if (isFirstInColumn(item)) {
\r
909 return getVerticalSibling (item, -1);
\r
913 this.moveItemUp = function (item) {
\r
914 var siblingAbove = this.getItemAbove(item);
\r
915 moveBefore (item, siblingAbove);
\r
918 this.getItemBelow = function (item) {
\r
919 if (isLastInColumn(item)) {
\r
922 return getVerticalSibling (item, 1);
\r
926 this.moveItemDown = function (item) {
\r
927 var siblingBelow = this.getItemBelow(item);
\r
928 moveAfter (item, siblingBelow);
\r
931 this.isMouseBefore = function(evt, droppableEl) {
\r
932 return isMouseBefore(evt, droppableEl, orientation);
\r
935 this.mouseMoveItem = function (e, item, relatedItem) {
\r
936 if (this.isMouseBefore (e, relatedItem)) {
\r
937 moveBefore (item, relatedItem);
\r
939 moveAfter (item, relatedItem);
\r
943 }; // End PortalLayoutHandler
\r