changed git call from https to git readonly
[atutor.git] / mods / photo_album / fluid / component-templates / js / fluid / Reorderer.js
1 /*\r
2 Copyright 2007 - 2008 University of Toronto\r
3 Copyright 2007 University of Cambridge\r
4 \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
7 Licenses.\r
8 \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
11 */\r
12 \r
13 // Declare dependencies.\r
14 var fluid = fluid || {};\r
15 \r
16 fluid.Reorderer = function (container, params) {\r
17     // Reliable 'this'.\r
18     //\r
19     var thisReorderer = this;\r
20     var theAvatar = null;\r
21     \r
22     this.domNode = jQuery (container);\r
23     \r
24     // the reorderable DOM element that is currently active\r
25     this.activeItem = null;\r
26         \r
27     this.layoutHandler = null;\r
28         \r
29     this.messageNamebase = "message-bundle:";\r
30     \r
31     this.orderableFinder = null;\r
32             \r
33     this.cssClasses = {\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
41     };\r
42 \r
43     if (params) {\r
44         fluid.mixin (this, params);\r
45     }\r
46     \r
47    /**\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
50     * \r
51     * @param {Object} item\r
52     * @return {Object} The element that should receive focus in the specified item.\r
53     */\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
58         }\r
59         return item;\r
60     };\r
61     \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
68     }   \r
69     \r
70     /**\r
71      * Changes the current focus to the specified item.\r
72      * @param {Object} anItem\r
73      */\r
74     this.focusItem = function(anItem) {\r
75         if (this.activeItem !== anItem) {\r
76             this.changeActiveItemToDefaultState();\r
77             this._setActiveItem (anItem);   \r
78         }\r
79         \r
80         var jActiveItem = jQuery (this.activeItem);\r
81         jActiveItem.removeClass (this.cssClasses.defaultStyle);\r
82         jActiveItem.addClass (this.cssClasses.selected);\r
83         \r
84         this.addFocusToElement (this.findElementToFocus (this.activeItem));\r
85     };\r
86     \r
87     this.addFocusToElement = function (anElement) {\r
88         var jElement = jQuery (anElement);\r
89         if (!jElement.hasTabIndex ()) {\r
90             jElement.tabIndex (-1);\r
91         }\r
92         jElement.focus ();\r
93     };\r
94     \r
95     this.removeFocusFromElement = function (anElement) {\r
96         jQuery (anElement).blur ();\r
97     };\r
98 \r
99     /**\r
100      * Changes focus to the active item.\r
101      */\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
107             }\r
108             else {\r
109                 return;\r
110             }\r
111         }\r
112         this.focusItem (this.activeItem);\r
113     };\r
114     \r
115     this.handleFocus = function (evt) {\r
116         thisReorderer.selectActiveItem();\r
117         return false;\r
118     };\r
119     \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
124         }\r
125     };\r
126             \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
133         }\r
134     };\r
135     \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
141            return false;\r
142        }\r
143 \r
144        return thisReorderer.handleArrowKeyPress(evt);\r
145     };\r
146 \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
152            return false;\r
153        } \r
154     };\r
155         \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
162                     return false;\r
163                 case fluid.keys.UP: \r
164                     evt.preventDefault();\r
165                     thisReorderer.handleUpArrow (evt.ctrlKey);                              \r
166                     return false;\r
167                 case fluid.keys.LEFT: \r
168                     evt.preventDefault();\r
169                     thisReorderer.handleLeftArrow (evt.ctrlKey);                                \r
170                     return false;\r
171                 case fluid.keys.RIGHT: \r
172                     evt.preventDefault();\r
173                     thisReorderer.handleRightArrow (evt.ctrlKey);                               \r
174                     return false;\r
175                 default:\r
176                     return true;\r
177             }\r
178         }\r
179     };\r
180     \r
181     this.handleUpArrow = function (isCtrl) {\r
182         if (isCtrl) {\r
183             this.layoutHandler.moveItemUp (this.activeItem);\r
184             this.findElementToFocus (this.activeItem).focus();\r
185         } else {\r
186             this.focusItem (this.layoutHandler.getItemAbove(this.activeItem));\r
187         }           \r
188     };\r
189     \r
190     this.handleDownArrow = function (isCtrl) {\r
191         if (isCtrl) {\r
192             this.layoutHandler.moveItemDown (this.activeItem);\r
193             this.findElementToFocus (this.activeItem).focus();\r
194         } else {\r
195             this.focusItem (this.layoutHandler.getItemBelow (this.activeItem));\r
196         }\r
197     };\r
198     \r
199     this.handleRightArrow = function (isCtrl) {\r
200         if (isCtrl) {\r
201             this.layoutHandler.moveItemRight (this.activeItem);\r
202             jQuery(this.findElementToFocus (this.activeItem)).focus();\r
203         } else {\r
204             this.focusItem (this.layoutHandler.getRightSibling (this.activeItem));              \r
205         }           \r
206     };\r
207     \r
208     this.handleLeftArrow = function (isCtrl) {\r
209         if (isCtrl) {\r
210             this.layoutHandler.moveItemLeft (this.activeItem);\r
211             this.findElementToFocus (this.activeItem).focus();\r
212         } else {\r
213             this.focusItem (this.layoutHandler.getLeftSibling (this.activeItem));               \r
214         }\r
215     };\r
216             \r
217     this._fetchMessage = function (messagekey) {\r
218         var messageID = this.messageNamebase + messagekey;\r
219         var node = document.getElementById (messageID);\r
220         \r
221         return node? node.innerHTML: "[Message not found at id " + messageID + "]";\r
222     };\r
223     \r
224     this._setActiveItem = function (anItem) {\r
225         this.activeItem = anItem;\r
226         this._updateActiveDescendent();\r
227     };\r
228     \r
229     this._updateActiveDescendent = function() {\r
230         if (this.activeItem) {\r
231             this.domNode.attr ("aaa:activedescendent", this.activeItem.id);\r
232         } else {\r
233             this.domNode.removeAttr ("aaa:activedescendent");\r
234         }\r
235     };\r
236 \r
237     var dropMarker; // private scratch variable\r
238     \r
239     /**\r
240      * evt.data - the droppable DOM element.\r
241      */\r
242     function trackMouseMovement (evt) {\r
243         if (thisReorderer.layoutHandler.isMouseBefore (evt, evt.data)) {\r
244            jQuery (evt.data).before (dropMarker);\r
245        }        \r
246        else {\r
247            jQuery (evt.data).after (dropMarker);\r
248        }\r
249     }\r
250 \r
251     /**\r
252      * Given an item, make it draggable.\r
253      */\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
263                 return theAvatar;\r
264             },\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
269                 \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
276             },\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
283                 }\r
284                 theAvatar = null;\r
285             }\r
286         });\r
287     \r
288     }   // end setUpDraggable()\r
289 \r
290     /**\r
291      * Make item a drop target.\r
292      */\r
293     function setUpDroppable (anItem, selector) {\r
294         anItem.droppable ({\r
295             accept: selector,\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
302             },\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
307             },\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
312             }\r
313         });\r
314     \r
315     }   // end setUpDroppable().\r
316  \r
317     /**\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
321      */ \r
322     function setUpDnDItem (anItem, selector) {\r
323         anItem.mouseover ( \r
324             function () {\r
325                 jQuery (this).addClass (thisReorderer.cssClasses.hover);\r
326             }\r
327         );\r
328         \r
329         anItem.mouseout (  \r
330             function () {\r
331                 jQuery (this).removeClass (thisReorderer.cssClasses.hover);\r
332             }\r
333         );\r
334   \r
335         // Make anItem draggable and a drop target (in the jQuery UI sense).\r
336         setUpDraggable (anItem);\r
337         setUpDroppable (anItem, selector);\r
338             \r
339     }   // end setUpDnDItem()\r
340         \r
341     function initOrderables() {\r
342         var items = thisReorderer.orderableFinder (thisReorderer.domNode.get(0));\r
343         if (items.length === 0) {\r
344             return;\r
345         }\r
346         \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
351         var selector = "";\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
356                 selector += ", ";\r
357             }                   \r
358         }\r
359 \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
364         }\r
365         \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
373                 }\r
374             }\r
375         }\r
376     }   // end initOrderables().\r
377 \r
378     /**\r
379      * Finds the "orderable" parent element given a child element.\r
380      */\r
381     this._findReorderableParent = function (childElement, items) {\r
382         if (!childElement) {\r
383             return null;\r
384         }\r
385         else {\r
386             for (var i=0; i<items.length; i++) {\r
387                 if (childElement === items[i]) {\r
388                     return childElement;\r
389                 }  \r
390             }\r
391             return this._findReorderableParent (childElement.parentNode, items);\r
392         }\r
393     };\r
394 \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
398         initOrderables();\r
399     }\r
400 }; // End Reorderer\r
401 \r
402 /*******************\r
403  * Layout Handlers *\r
404  *******************/\r
405 (function () {\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
411         }\r
412         \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
417         } else {\r
418             jQuery (relatedItemInfo.item).before (item);\r
419         } \r
420     };\r
421     \r
422     /**\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
426      * "left of".\r
427      */\r
428     var isMouseBefore = function (evt, droppableEl, orientation) {\r
429         var mid;\r
430         if (orientation === fluid.orientation.VERTICAL) {\r
431             mid = jQuery (droppableEl).offset().top + (droppableEl.offsetHeight / 2);\r
432             return (evt.pageY < mid);\r
433         } else {\r
434             mid = jQuery (droppableEl).offset().left + (droppableEl.offsetWidth / 2);\r
435             return (evt.clientX < mid);\r
436         }\r
437     };    \r
438 \r
439     var itemInfoFinders = {\r
440         /*\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
445          */\r
446         getSiblingInfo: function (item, orderables, /* +1, -1 */ incDecrement) {\r
447             var index = jQuery (orderables).index (item) + incDecrement;\r
448             var hasWrapped = false;\r
449                 \r
450             // Handle wrapping to 'before' the beginning. \r
451             if (index === -1) {\r
452                 index = orderables.length - 1;\r
453                 hasWrapped = true;\r
454             }\r
455             // Handle wrapping to 'after' the end.\r
456             else if (index === orderables.length) {\r
457                 index = 0;\r
458                 hasWrapped = true;\r
459             } \r
460             // Handle case where the passed-in item is *not* an "orderable"\r
461             // (or other undefined error).\r
462             //\r
463             else if (index < -1 || index > orderables.length) {\r
464                 index = 0;\r
465             }\r
466             \r
467             return {item: orderables[index], hasWrapped: hasWrapped};\r
468         },\r
469 \r
470         /*\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
474          */\r
475         getRightSiblingInfo: function (item, orderables) {\r
476             return this.getSiblingInfo (item, orderables, 1);\r
477         },\r
478         \r
479         /*\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
483          */\r
484         getLeftSiblingInfo: function (item, orderables) {\r
485             return this.getSiblingInfo (item, orderables, -1);\r
486         },\r
487         \r
488         /*\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
494          */\r
495         getItemInfoBelow: function (inItem, orderables) {\r
496             var curCoords = jQuery (inItem).offset();\r
497             var i, iCoords;\r
498             var firstItemInColumn, currentItem;\r
499             \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
507                     }\r
508                 }\r
509             }\r
510     \r
511             firstItemInColumn = firstItemInColumn || orderables [0];\r
512             return {item: firstItemInColumn, hasWrapped: true};\r
513         },\r
514         \r
515         /*\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
521          */\r
522          getItemInfoAbove: function (inItem, orderables) {\r
523             var curCoords = jQuery (inItem).offset();\r
524             var i, iCoords;\r
525             var lastItemInColumn, currentItem;\r
526             \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
534                     }\r
535                 }\r
536             }\r
537     \r
538             lastItemInColumn = lastItemInColumn || orderables [0];\r
539             return {item: lastItemInColumn, hasWrapped: true};\r
540         }\r
541     \r
542     };\r
543     \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
550         \r
551         // Unwrap the container if it's a jQuery.\r
552         container = (container.jquery) ? container.get(0) : container;\r
553         \r
554         orderChangedCallback = orderChangedCallback || function () {};\r
555         \r
556         orientation = orientation || fluid.orientation.VERTICAL;    // default\r
557                 \r
558         this.getRightSibling = function (item) {\r
559             return itemInfoFinders.getRightSiblingInfo(item, orderableFinder(container)).item;\r
560         };\r
561         \r
562         this.moveItemRight = function (item) {\r
563             moveItem (item, itemInfoFinders.getRightSiblingInfo(item, orderableFinder(container)), "after", "before");\r
564             orderChangedCallback();\r
565         };\r
566     \r
567         this.getLeftSibling = function (item) {\r
568             return itemInfoFinders.getLeftSiblingInfo(item, orderableFinder(container)).item;\r
569         };\r
570     \r
571         this.moveItemLeft = function (item) {\r
572             moveItem (item, itemInfoFinders.getLeftSiblingInfo(item, orderableFinder(container)), "before", "after");\r
573             orderChangedCallback();\r
574         };\r
575     \r
576         this.getItemBelow = this.getRightSibling;\r
577     \r
578         this.getItemAbove = this.getLeftSibling;\r
579         \r
580         this.moveItemUp = this.moveItemLeft;\r
581         \r
582         this.moveItemDown = this.moveItemRight;\r
583     \r
584         this.isMouseBefore = function(evt, droppableEl) {\r
585             return isMouseBefore(evt, droppableEl, orientation);\r
586         };\r
587         \r
588         this.mouseMoveItem = function (e, item, relatedItem) {\r
589             if (this.isMouseBefore (e, relatedItem)) {\r
590                 jQuery (relatedItem).before (item);\r
591             } else {\r
592                 jQuery (relatedItem).after (item);\r
593             }\r
594             orderChangedCallback(); \r
595         };\r
596     }; // End ListLayoutHandler\r
597     \r
598         /*\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
602          * \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
605          */\r
606         fluid.GridLayoutHandler = function (params) {\r
607         fluid.ListLayoutHandler.call (this, params);\r
608 \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
613         \r
614                 // Unwrap the container if it's a jQuery.\r
615         container = (container.jquery) ? container.get(0) : container;\r
616         \r
617         orderChangedCallback = orderChangedCallback || function () {};\r
618         \r
619             this.getItemBelow = function(item) {\r
620                 return itemInfoFinders.getItemInfoBelow (item, orderableFinder(container)).item;\r
621             };\r
622         \r
623             this.moveItemDown = function (item) {\r
624                 moveItem (item, itemInfoFinders.getItemInfoBelow (item, orderableFinder(container)), "after", "before");\r
625             orderChangedCallback(); \r
626             };\r
627                     \r
628             this.getItemAbove = function (item) {\r
629                 return itemInfoFinders.getItemInfoAbove (item, orderableFinder(container)).item;   \r
630             }; \r
631             \r
632             this.moveItemUp = function (item) {\r
633                 moveItem (item, itemInfoFinders.getItemInfoAbove (item, orderableFinder(container)), "before", "after");\r
634             orderChangedCallback(); \r
635             };\r
636                         \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
641         };\r
642         \r
643         }; // End of GridLayoutHandler\r
644 \r
645     fluid.portletPerms = fluid.portletPerms || {};\r
646     fluid.portletPerms.canDrop = function (perms, indexOfItem, indexOfTarget, position) {\r
647         return (!!perms[indexOfItem][indexOfTarget][position]); \r
648     };\r
649     \r
650     /*\r
651      * PortletLayout helper object.\r
652      */\r
653     fluid.PortletLayout = function() {\r
654                 \r
655         /**\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
659          */\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
667                     break;\r
668                 }                \r
669             }\r
670             return indices;\r
671         };\r
672                         \r
673         /**\r
674          * Return the first orderable item in the given column.\r
675          */\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
679             var itemIndex = 0;\r
680             var column = portletLayoutJSON.columns[columnIndex];\r
681             if (column) {\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
686                     itemIndex += 1;\r
687                     id = column.children[itemIndex];\r
688                     topMostOrderableSibling = jQuery ("#" + id).get (0);\r
689                 }\r
690             }\r
691             return topMostOrderableSibling;\r
692         };\r
693         \r
694         /**\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
697          */\r
698         this.numItemsInColumn = function (columnIndex, portletLayoutJSON) {\r
699                 if ((columnIndex < 0) || (columnIndex > portletLayoutJSON.columns.length)) {\r
700                         return -1;\r
701                 }\r
702                 else {\r
703                    return portletLayoutJSON.columns[columnIndex].children.length;\r
704                 }\r
705         };\r
706         \r
707         this.numColumns = function (layout) {\r
708             return layout.columns.length;\r
709         };\r
710         \r
711         this.findLinearIndex = function (itemId, layout) {\r
712             var columns = layout.columns;\r
713             var linearIndex = 0;\r
714             \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
720                         }\r
721                         linearIndex++;\r
722                 }\r
723             }\r
724             \r
725             return -1;\r
726         };\r
727         \r
728         /**\r
729          * Move an item within the portletLayoutJSON object. \r
730          */\r
731         this.updateLayout = function (item, relativeItem, placement, portletLayoutJSON) {\r
732             if (!item || !relativeItem) { return; }\r
733             var itemIndices = this.calcColumnAndItemIndex (item, portletLayoutJSON);\r
734 \r
735             var itemId = portletLayoutJSON.columns[itemIndices.columnIndex].children[itemIndices.itemIndex];\r
736             portletLayoutJSON.columns[itemIndices.columnIndex].children.splice (itemIndices.itemIndex, 1);\r
737 \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
741         };\r
742     };   // End of PortletLayout.\r
743     \r
744     /*\r
745      * Portlet Layout Handler for reordering portlet-type markup.\r
746      * \r
747      * General movement guidelines:\r
748      * \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
752      */\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
760         \r
761         var portletLayout = new fluid.PortletLayout();\r
762         \r
763         // Unwrap the container if it's a jQuery.\r
764         container = (container.jquery) ? container.get(0) : container;\r
765         \r
766         orderChangedCallback = orderChangedCallback || function () {};\r
767 \r
768         // Private Methods.\r
769         /*\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
775              */\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
779                     \r
780                 // If we wrap, backup \r
781                 if ((index === -1) || (index === orderables.length)) {\r
782                     return null;\r
783                 }\r
784                 // Handle case where the passed-in item is *not* an "orderable"\r
785                 // (or other undefined error).\r
786                 //\r
787                 else if (index < -1 || index > orderables.length) {\r
788                     index = 0;\r
789                 }\r
790                 \r
791                 return orderables[index];\r
792             };\r
793         \r
794             /*\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
797              * move left.\r
798              * Currently, the horizontal sibling defaults to the top orderable item in the\r
799              * neighboring column.\r
800              */\r
801             var getHorizontalSibling = function (item, /* +1, -1 */ incDecrement) {\r
802                 var orderables = orderableFinder (container);\r
803                     \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
808                     return null;\r
809                 }\r
810             var sibling = portletLayout.findFirstOrderableSiblingInColumn (colIndex + incDecrement, orderables, portletLayoutJSON);\r
811                 return sibling;\r
812         \r
813             };\r
814                     \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
819         };\r
820     \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
824         };\r
825         \r
826         var isInLeftmostColumn = function (item) {\r
827             if (portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON).columnIndex === 0) {\r
828                 return true;\r
829             } else {\r
830                 return false;\r
831             }\r
832         };\r
833         \r
834         var isInRightmostColumn = function (item) {\r
835             if (portletLayout.calcColumnAndItemIndex (item, portletLayoutJSON).columnIndex === portletLayout.numColumns (portletLayoutJSON) - 1) {\r
836                 return true;\r
837             } else {\r
838                 return false;\r
839             }\r
840         };\r
841             \r
842         var canDrop = function (itemId, relatedItemId, position) {\r
843             return fluid.portletPerms.canDrop(\r
844                 targetPerms, \r
845                 portletLayout.findLinearIndex(itemId, portletLayoutJSON), \r
846                 portletLayout.findLinearIndex(relatedItemId, portletLayoutJSON), \r
847                 position);\r
848         };\r
849         \r
850         var moveBefore = function (item, relatedItem) {\r
851             if (!canDrop (item.id, relatedItem.id, fluid.position.BEFORE)) {\r
852                 return;\r
853             } \r
854                 \r
855             jQuery (relatedItem).before (item);\r
856             portletLayout.updateLayout (item, relatedItem, "before", portletLayoutJSON);\r
857             portletLayoutJSON = orderChangedCallback() || portletLayoutJSON; \r
858         };\r
859         \r
860         var moveAfter = function (item, relatedItem) {\r
861             if (!canDrop (item.id, relatedItem.id, fluid.position.AFTER)) {\r
862                 return;\r
863             } \r
864                 \r
865             jQuery (relatedItem).after (item);\r
866             portletLayout.updateLayout (item, relatedItem, "after", portletLayoutJSON);\r
867             portletLayoutJSON = orderChangedCallback() || portletLayoutJSON; \r
868         };         \r
869 \r
870         // Public Methods\r
871         \r
872             this.getRightSibling = function (item) {\r
873             if (isInRightmostColumn(item)) {\r
874                 return item;\r
875             } else {\r
876                 return getHorizontalSibling(item, 1);\r
877             }\r
878             };\r
879             \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
888             };\r
889         \r
890             this.getLeftSibling = function (item) {\r
891             if (isInLeftmostColumn(item)) {\r
892                 return item;\r
893             } else {\r
894                 return getHorizontalSibling(item, -1);\r
895             }\r
896             };\r
897         \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
903             };\r
904         \r
905             this.getItemAbove = function (item) {\r
906                 if (isFirstInColumn(item)) {\r
907                 return item;\r
908             } else {\r
909                 return getVerticalSibling (item, -1);\r
910             }\r
911             };\r
912             \r
913             this.moveItemUp = function (item) {\r
914                 var siblingAbove = this.getItemAbove(item);\r
915                 moveBefore (item, siblingAbove);\r
916             };\r
917                 \r
918             this.getItemBelow = function (item) {\r
919                 if (isLastInColumn(item)) {\r
920                 return item;\r
921             } else {\r
922                 return getVerticalSibling (item, 1);\r
923             }\r
924             };\r
925         \r
926             this.moveItemDown = function (item) {\r
927                 var siblingBelow = this.getItemBelow(item);\r
928                 moveAfter (item, siblingBelow);\r
929             };\r
930             \r
931             this.isMouseBefore = function(evt, droppableEl) {\r
932             return isMouseBefore(evt, droppableEl, orientation);\r
933         };\r
934 \r
935         this.mouseMoveItem = function (e, item, relatedItem) {\r
936             if (this.isMouseBefore (e, relatedItem)) {\r
937                 moveBefore (item, relatedItem);\r
938             } else {\r
939                 moveAfter (item, relatedItem);\r
940             }\r
941         };\r
942         \r
943     }; // End PortalLayoutHandler\r
944 }) ();\r
945 \r