// Title: groups.js (revision-a) (function(){ var numCmp = function(a,b){ return a-b; } function min(list){ return list.slice().sort(numCmp)[0]; } function max(list){ return list.slice().sort(numCmp).reverse()[0]; } function isEventOverElement(event, el){ var hit = {nodeName: null}; var isOver = false; var hiddenEls = []; while(hit.nodeName != "BODY" && hit.nodeName != "HTML"){ hit = document.elementFromPoint(event.clientX, event.clientY); if( hit == el ){ isOver = true; break; } $(hit).hide(); hiddenEls.push(hit); } var hidden; [$(hidden).show() for([,hidden] in Iterator(hiddenEls))]; return isOver; } // ########## // Class: Group // A single group in the tab candy window. Descended from . // Note that it implements the interface. window.Group = function(listOfEls, options) { if(typeof(options) == 'undefined') options = {}; this._children = []; // an array of Items this.defaultSize = new Point(TabItems.tabWidth * 1.5, TabItems.tabHeight * 1.5); this.locked = options.locked || false; this.isAGroup = true; this.id = options.id || Groups.getNextID(); this.userSize = options.userSize || null; this._isStacked = false; this._stackAngles = [0]; this.expanded = null; var self = this; var rectToBe; if(options.bounds) rectToBe = new Rect(options.bounds); if(!rectToBe) { var boundingBox = this._getBoundingBox(listOfEls); var padding = 30; rectToBe = new Rect( boundingBox.left-padding, boundingBox.top-padding, boundingBox.width+padding*2, boundingBox.height+padding*2 ); } var $container = options.container; if(!$container) { $container = $('
') .css({position: 'absolute'}) .css(rectToBe); } $container .css({zIndex: -100}) .appendTo("body") .dequeue(); // ___ New Tab Button this.$ntb = $("
").appendTo($container); this.$ntb.click(function(){ self.newTab(); }); // ___ Resizer this.$resizer = $("
") .css({ position: "absolute", width: 16, height: 16, bottom: 0, right: 0, }) .appendTo($container) .hide(); // ___ Titlebar var html = "
"; this.$titlebar = $(html) .appendTo($container); this.$titlebar.css({ position: "absolute", }); var $close = $('.close', this.$titlebar).click(function() { self.closeAll(); }); // ___ Title var titleUnfocus = function() { if(!self.getTitle()) { self.$title .addClass("defaultName") .val(self.defaultName); } else { self.$title.css({"background":"none"}) .animate({"paddingLeft":1}, 340, "tabcandyBounce"); } }; var handleKeyPress = function(e){ if( e.which == 13 ) { // return self.$title.blur() .addClass("transparentBorder") .one("mouseout", function(){ self.$title.removeClass("transparentBorder"); }); } else self.adjustTitleSize(); } this.$title = $('.name', this.$titlebar) .css({backgroundRepeat: 'no-repeat'}) .blur(titleUnfocus) .focus(function() { if(self.locked) { self.$title.blur(); return; } self.$title.select(); if(!self.getTitle()) { self.$title .removeClass("defaultName") .val(''); } }) .keyup(handleKeyPress); titleUnfocus(); if(this.locked) this.$title.addClass('name-locked'); // ___ Content this.$content = $('
') .css({ left: 0, top: this.$titlebar.height(), position: 'absolute' }) .appendTo($container); // ___ locking if(this.locked) { $container.css({cursor: 'default'}); /* $close.hide(); */ } // ___ Superclass initialization this._init($container.get(0)); if(this.$debug) this.$debug.css({zIndex: -1000}); // ___ Children $.each(listOfEls, function(index, el) { self.add(el, null, options); }); // ___ Finish Up this._addHandlers($container); if(!this.locked) this.setResizable(true); Groups.register(this); this.setBounds(rectToBe); // ___ Push other objects away if(!options.dontPush) this.pushAway(); }; // ---------- window.Group.prototype = $.extend(new Item(), new Subscribable(), { // ---------- defaultName: "name this group...", // ---------- getStorageData: function() { var data = { bounds: this.getBounds(), userSize: this.userSize, locked: this.locked, title: this.getTitle(), id: this.id }; return data; }, // ---------- getTitle: function() { var value = (this.$title ? this.$title.val() : ''); return (value == this.defaultName ? '' : value); }, // ---------- setTitle: function(value) { this.$title.val(value); }, // ---------- adjustTitleSize: function() { Utils.assert('bounds needs to have been set', this.bounds); var w = Math.min(this.bounds.width - 35, Math.max(150, this.getTitle().length * 6)); this.$title.css({ width: w }); }, // ---------- _getBoundingBox: function(els) { var el; var boundingBox = { top: min( [$(el).position().top for([,el] in Iterator(els))] ), left: min( [$(el).position().left for([,el] in Iterator(els))] ), bottom: max( [$(el).position().top for([,el] in Iterator(els))] ) + $(els[0]).height(), right: max( [$(el).position().left for([,el] in Iterator(els))] ) + $(els[0]).width(), }; boundingBox.height = boundingBox.bottom - boundingBox.top; boundingBox.width = boundingBox.right - boundingBox.left; return boundingBox; }, // ---------- getContentBounds: function() { var box = this.getBounds(); var titleHeight = this.$titlebar.height(); box.top += titleHeight; box.height -= titleHeight; box.inset(6, 6); box.height -= 15; // For new tab button return box; }, // ---------- reloadBounds: function() { var bb = Utils.getBounds(this.container); if(!this.bounds) this.bounds = new Rect(0, 0, 0, 0); this.setBounds(bb, true); }, // ---------- setBounds: function(rect, immediately) { var titleHeight = this.$titlebar.height(); // ___ Determine what has changed var css = {}; var titlebarCSS = {}; var contentCSS = {}; if(rect.left != this.bounds.left) css.left = rect.left; if(rect.top != this.bounds.top) css.top = rect.top; if(rect.width != this.bounds.width) { css.width = rect.width; titlebarCSS.width = rect.width; contentCSS.width = rect.width; } if(rect.height != this.bounds.height) { css.height = rect.height; contentCSS.height = rect.height - titleHeight; } if($.isEmptyObject(css)) return; var offset = new Point(rect.left - this.bounds.left, rect.top - this.bounds.top); this.bounds = new Rect(rect); // ___ Deal with children if(this._children.length) { if(css.width || css.height) { this.arrange({animate: !immediately}); //(immediately ? 'sometimes' : true)}); } else if(css.left || css.top) { $.each(this._children, function(index, child) { var box = child.getBounds(); child.setPosition(box.left + offset.x, box.top + offset.y, immediately); }); } } // ___ Update our representation if(immediately) { $(this.container).css(css); this.$titlebar.css(titlebarCSS); this.$content.css(contentCSS); } else { TabMirror.pausePainting(); $(this.container).animate(css, { complete: function() {TabMirror.resumePainting();}, easing: "tabcandyBounce" }).dequeue(); this.$titlebar.animate(titlebarCSS).dequeue(); this.$content.animate(contentCSS).dequeue(); } this.adjustTitleSize(); this._updateDebugBounds(); }, // ---------- setZ: function(value) { $(this.container).css({zIndex: value}); if(this.$debug) this.$debug.css({zIndex: value + 1}); var count = this._children.length; if(count) { var zIndex = value + count + 1; $.each(this._children, function(index, child) { child.setZ(zIndex); zIndex--; }); } }, // ---------- close: function() { this.removeAll(); this._sendOnClose(); Groups.unregister(this); $(this.container).fadeOut(function() { $(this).remove(); }); }, // ---------- closeAll: function() { if(this._children.length) { var toClose = $.merge([], this._children); $.each(toClose, function(index, child) { child.close(); }); } else if(!this.locked) this.close(); }, // ---------- // Function: add // Adds an item to the group. // Parameters: // // a - The item to add. Can be an , a DOM element or a jQuery object. // The latter two must refer to the container of an . // dropPos - An object with left and top properties referring to the location dropped at. Optional. // options - An object with optional settings for this call. Currently the only one is dontArrange. add: function(a, dropPos, options) { var item; var $el; if(a.isAnItem) { item = a; $el = $(a.container); } else { $el = $(a); item = Items.item($el); } Utils.assert('shouldn\'t already be in a group', !item.parent || item.parent == this); if(!dropPos) dropPos = {top:window.innerWidth, left:window.innerHeight}; if(typeof(options) == 'undefined') options = {}; var self = this; var wasAlreadyInThisGroup = false; var oldIndex = $.inArray(item, this._children); if(oldIndex != -1) { this._children.splice(oldIndex, 1); wasAlreadyInThisGroup = true; } // TODO: You should be allowed to drop in the white space at the bottom and have it go to the end // (right now it can match the thumbnail above it and go there) function findInsertionPoint(dropPos){ if(self.shouldStack(self._children.length + 1)) return 0; var best = {dist: Infinity, item: null}; var index = 0; var box; $.each(self._children, function(index, child) { box = child.getBounds(); if(box.bottom < dropPos.top || box.top > dropPos.top) return; var dist = Math.sqrt( Math.pow((box.top+box.height/2)-dropPos.top,2) + Math.pow((box.left+box.width/2)-dropPos.left,2) ); if( dist <= best.dist ){ best.item = child; best.dist = dist; best.index = index; } }); if( self._children.length > 0 ){ if(best.item) { box = best.item.getBounds(); var insertLeft = dropPos.left <= box.left + box.width/2; if( !insertLeft ) return best.index+1; else return best.index; } else return self._children.length; } return 0; } // Insert the tab into the right position. var index = findInsertionPoint(dropPos); this._children.splice( index, 0, item ); item.setZ(this.getZ() + 1); $el.addClass("tabInGroup"); if(!wasAlreadyInThisGroup) { $el.droppable("disable"); item.groupData = {}; item.addOnClose(this, function() { self.remove($el); }); item.parent = this; if(typeof(item.setResizable) == 'function') item.setResizable(false); } if(!options.dontArrange) this.arrange(); if( this._nextNewTabCallback ){ this._nextNewTabCallback.apply(this, [item]) this._nextNewTabCallback = null; } }, // ---------- // Function: remove // Removes an item from the group. // Parameters: // // a - The item to remove. Can be an , a DOM element or a jQuery object. // The latter two must refer to the container of an . // options - An object with optional settings for this call. Currently the only one is dontArrange. remove: function(a, options) { var $el; var item; if(a.isAnItem) { item = a; $el = $(item.container); } else { $el = $(a); item = Items.item($el); } if(typeof(options) == 'undefined') options = {}; var index = $.inArray(item, this._children); if(index != -1) this._children.splice(index, 1); item.parent = null; $el.removeClass("tabInGroup"); item.setSize(item.defaultSize.x, item.defaultSize.y); $el.droppable("enable"); item.removeOnClose(this); if(typeof(item.setResizable) == 'function') item.setResizable(true); if(this._children.length == 0 && !this.locked && !this.getTitle()){ this.close(); } else if(!options.dontArrange) { this.arrange(); } }, // ---------- removeAll: function() { var self = this; var toRemove = $.merge([], this._children); $.each(toRemove, function(index, child) { self.remove(child, {dontArrange: true}); }); }, // ---------- shouldStack: function(count) { var bb = this.getContentBounds(); var result = !(count == 1 || (bb.width * bb.height) / count > 7000); return result; }, // ---------- arrange: function(options) { if(this.expanded) return; var count = this._children.length; if(!count) return; var bb = this.getContentBounds(); if(!this.shouldStack(count)) { this._children.forEach(function(child){ child.removeClass("stacked") }); Items.arrange(this._children, bb, options); this._isStacked = false; } else this._stackArrange(bb, options); }, // ---------- _stackArrange: function(bb, options) { var animate; if(!options || typeof(options.animate) == 'undefined') animate = true; else animate = options.animate; if(typeof(options) == 'undefined') options = {}; var count = this._children.length; if(!count) return; var zIndex = this.getZ() + count + 1; var scale = 0.8; var w; var h; var itemAspect = TabItems.tabHeight / TabItems.tabWidth; var bbAspect = bb.height / bb.width; if(bbAspect > itemAspect) { // Tall, thin group w = bb.width * scale; h = w * itemAspect; } else { // Short, wide group h = bb.height * scale; w = h * (1 / itemAspect); } var x = (bb.width - w) / 2; var y = Math.min(x, (bb.height - h) / 2); var box = new Rect(bb.left + x, bb.top + y, w, h); var self = this; $.each(this._children, function(index, child) { if(!child.locked) { child.setZ(zIndex); zIndex--; child.setBounds(box, !animate); child.setRotation(self._randRotate(35, index)); child.addClass("stacked"); } }); self._isStacked = true; }, // ---------- _randRotate: function(spread, index){ if( index >= this._stackAngles.length ){ var randAngle = parseInt( ((Math.random()+.6)/1.3)*spread-(spread/2) ); this._stackAngles.push(randAngle); return randAngle; } return this._stackAngles[index]; }, // ---------- childHit: function(child) { var self = this; // ___ normal click if(!this._isStacked || this.expanded) { setTimeout(function() { self.collapse(); }, 200); return false; } // ___ we're stacked, so expand Groups.setActiveGroup(self); var startBounds = child.getBounds(); var $tray = $("
").css({ top: startBounds.top, left: startBounds.left, width: startBounds.width, height: startBounds.height, position: "absolute", zIndex: 99998 }).appendTo("body"); var w = 180; var h = w * (TabItems.tabHeight / TabItems.tabWidth) * 1.1; var padding = 20; var col = Math.ceil(Math.sqrt(this._children.length)); var row = Math.ceil(this._children.length/col); var overlayWidth = Math.min(window.innerWidth - (padding * 2), w*col + padding*(col+1)); var overlayHeight = Math.min(window.innerHeight - (padding * 2), h*row + padding*(row+1)); var pos = {left: startBounds.left, top: startBounds.top}; pos.left -= overlayWidth/3; pos.top -= overlayHeight/3; if( pos.top < 0 ) pos.top = 20; if( pos.left < 0 ) pos.left = 20; if( pos.top+overlayHeight > window.innerHeight ) pos.top = window.innerHeight-overlayHeight-20; if( pos.left+overlayWidth > window.innerWidth ) pos.left = window.innerWidth-overlayWidth-20; $tray.animate({ width: overlayWidth, height: overlayHeight, top: pos.top, left: pos.left }, 350, "tabcandyBounce").addClass("overlay"); this._children.forEach(function(child){ child.addClass("stack-trayed"); }); var box = new Rect(pos.left, pos.top, overlayWidth, overlayHeight); box.inset(8, 8); Items.arrange(this._children, box, {padding: 8, z: 99999}); var $shield = $('
') .css({ left: 0, top: 0, width: window.innerWidth, height: window.innerHeight, position: 'absolute', zIndex: 99997 }) .appendTo('body') .mouseover(function() { self.collapse(); }) .click(function() { // just in case self.collapse(); }); this.expanded = { $tray: $tray, $shield: $shield }; return true; }, // ---------- collapse: function() { if(this.expanded) { var z = this.getZ(); var box = this.getBounds(); this.expanded.$tray .css({ zIndex: z + 1 }) .animate({ width: box.width, height: box.height, top: box.top, left: box.left, opacity: 0 }, 350, "tabcandyBounce", function() { $(this).remove(); }); this.expanded.$shield.remove(); this.expanded = null; this._children.forEach(function(child){ child.removeClass("stack-trayed"); }); this.arrange({z: z + 2}); } }, // ---------- _addHandlers: function(container) { var self = this; if(!this.locked) { $(container).draggable({ scroll: false, cancel: '.close, .name', start: function(e, ui){ drag.info = new DragInfo(this, e); }, drag: function(e, ui){ drag.info.drag(e, ui); }, stop: function() { drag.info.stop(); drag.info = null; } }); } $(container).droppable({ tolerance: "intersect", over: function(){ drag.info.$el.addClass("willGroup"); }, out: function(){ var group = drag.info.item.parent; if(group) { group.remove(drag.info.$el); } drag.info.$el.removeClass("willGroup"); }, drop: function(event){ drag.info.$el.removeClass("willGroup"); self.add( drag.info.$el, {left:event.pageX, top:event.pageY} ); }, accept: ".tab", //".tab, .group", }); }, // ---------- setResizable: function(value){ var self = this; if(value) { this.$resizer.fadeIn(); $(this.container).resizable({ handles: "se", aspectRatio: false, minWidth: 90, minHeight: 90, resize: function(){ self.reloadBounds(); }, stop: function(){ self.reloadBounds(); self.setUserSize(); self.pushAway(); } }); } else { this.$resizer.fadeOut(); $(this.container).resizable('disable'); } }, // ---------- newTab: function() { Groups.setActiveGroup(this); var newTab = Tabs.open("about:blank"); UI.navBar.hide(); var self = this; var doNextTab = function(tab){ var group = Groups.getActiveGroup(); $(tab.container).css({opacity: 0}); anim = $("
").css({ top: tab.bounds.top+5, left: tab.bounds.left+5, width: tab.bounds.width-10, height: tab.bounds.height-10, zIndex: 999, opacity: 0 }) .appendTo("body") .animate({ opacity: 1.0 },500) .animate({ top: 0, left: 0, width: window.innerWidth, height: window.innerHeight }, 270, function(){ $(tab.container).css({opacity: 1}); newTab.focus(); UI.tabBar.show(false); UI.navBar.show(); UI.navBar.urlBar.focus(); anim.remove(); // We need a timeout here so that there is a chance for the // new tab to get made! Otherwise it won't appear in the list // of the group's tab. // TODO: This is probably a terrible hack that sets up a race // condition. We need a better solution. setTimeout(function(){ UI.tabBar.showOnlyTheseTabs(Groups.getActiveGroup()._children); }, 400); }); } // TODO: Because this happens as a callback, there is // sometimes a long delay before the animation occurs. // We need to fix this--immediate response to a users // actions is necessary for a good user experience. self.onNextNewTab(doNextTab); }, // ---------- // Function: reorderBasedOnTabOrder // Reorderes the tabs in a group based on the arrangment of the tabs // shown in the tab bar. It doesn't it by sorting the children // of the group by the positions of their respective tabs in the // tab bar. reorderBasedOnTabOrder: function(){ var groupTabs = []; for( var i=0; i= this._children.length || index < 0 ) return null; return this._children[index]; }, // --------- // Function: onNextNewTab // Sets up a one-time handler that gets called the next time a // tab is added to the group. // // Parameters // callback - the one-time callback that is fired when the next // time a tab is added to a group; it gets passed the // new tab onNextNewTab: function(callback){ this._nextNewTabCallback = callback; } }); // ########## var DragInfo = function(element, event) { this.el = element; this.$el = $(this.el); this.item = Items.item(this.el); this.parent = this.item.parent; this.startPosition = new Point(event.clientX, event.clientY); this.startTime = Utils.getMilliseconds(); this.$el.data('isDragging', true); this.item.setZ(99999); }; DragInfo.prototype = { // ---------- drag: function(event, ui) { if(this.item.isAGroup) { var bb = this.item.getBounds(); bb.left = ui.position.left; bb.top = ui.position.top; this.item.setBounds(bb, true); } else this.item.reloadBounds(); if(this.parent && this.parent.expanded) { var now = Utils.getMilliseconds(); var distance = this.startPosition.distance(new Point(event.clientX, event.clientY)); if(now - this.startTime > 500 && distance > 100) { this.item.locked = true; this.parent.collapse(); this.item.locked = false; } } }, // ---------- stop: function() { this.$el.data('isDragging', false); if(this.parent && !this.parent.locked && this.parent != this.item.parent && this.parent._children.length == 1 && !this.parent.getTitle()) { this.parent.remove(this.parent._children[0]); } if(this.item && !this.$el.hasClass('willGroup') && !this.item.parent) { this.item.setZ(drag.zIndex); drag.zIndex++; this.item.reloadBounds(); this.item.pushAway(); } } }; var drag = { info: null, zIndex: 100 }; // ########## // Class: Groups // Singelton for managing all s. window.Groups = { // ---------- dragOptions: { scroll: false, cancel: '.close', start: function(e, ui) { drag.info = new DragInfo(this, e); }, drag: function(e, ui) { drag.info.drag(e, ui); }, stop: function() { drag.info.stop(); drag.info = null; } }, // ---------- dropOptions: { accept: ".tab", tolerance: "intersect", greedy: true, drop: function(e){ $target = $(e.target); drag.info.$el.removeClass("willGroup") var phantom = $target.data("phantomGroup") var group = drag.info.item.parent; if( group == null ){ phantom.removeClass("phantom"); phantom.removeClass("group-content"); var group = new Group([$target, drag.info.$el], {container:phantom}); } else group.add( drag.info.$el ); }, over: function(e){ var $target = $(e.target); function elToRect($el){ return new Rect( $el.position().left, $el.position().top, $el.width(), $el.height() ); } var height = elToRect($target).height * 1.5 + 20; var width = elToRect($target).width * 1.5 + 20; var unionRect = elToRect($target).union( elToRect(drag.info.$el) ); var newLeft = unionRect.left + unionRect.width/2 - width/2; var newTop = unionRect.top + unionRect.height/2 - height/2; $(".phantom").remove(); var phantom = $("
").css({ width: width, height: height, position:"absolute", top: newTop, left: newLeft, zIndex: -99 }).appendTo("body").hide().fadeIn(); $target.data("phantomGroup", phantom); }, out: function(e){ $(e.target).data("phantomGroup").fadeOut(function(){ $(this).remove(); }); } }, // ---------- init: function() { this.groups = []; this.nextID = 1; }, // ---------- getNextID: function() { var result = this.nextID; this.nextID++; return result; }, // ---------- getStorageData: function() { var data = {nextID: this.nextID, groups: []}; $.each(this.groups, function(index, group) { data.groups.push(group.getStorageData()); }); return data; }, // ---------- reconstitute: function(data) { if(data && data.nextID) this.nextID = data.nextID; if(data && data.groups) { $.each(data.groups, function(index, group) { new Group([], $.extend({}, group, {locked: false, dontPush: true})); }); } var group = this.getNewTabGroup(); if(!group) { var box = this.getBoundsForNewTabGroup(); new Group([], {bounds: box, title: 'New Tabs', dontPush: true}); } }, // ---------- getNewTabGroup: function() { var groupTitle = 'New Tabs'; var array = jQuery.grep(this.groups, function(group) { return group.getTitle() == groupTitle; }); if(array.length) return array[0]; return null; }, // ---------- getBoundsForNewTabGroup: function() { var pad = 20; var sw = window.innerWidth; var sh = window.innerHeight; //var w = sw - (pad * 2); var w = TabItems.tabWidth*2 + pad*2; var h = TabItems.tabHeight*1.2 + pad*2; return new Rect(pad, sh - (h + pad), w, h); }, // ---------- repositionNewTabGroup: function() { var box = this.getBoundsForNewTabGroup(); var group = this.getNewTabGroup(); group.setBounds(box, true); }, // ---------- register: function(group) { Utils.assert('only register once per group', $.inArray(group, this.groups) == -1); this.groups.push(group); }, // ---------- unregister: function(group) { var index = $.inArray(group, this.groups); if(index != -1) this.groups.splice(index, 1); if(group == this._activeGroup) this._activeGroup = null; }, // ---------- // Function: group // Given some sort of identifier, returns the appropriate group. // Currently only supports group ids. group: function(a) { var result = null; $.each(this.groups, function(index, candidate) { if(candidate.id == a) { result = candidate; return false; } }); return result; }, // ---------- arrange: function() { var count = this.groups.length; var columns = Math.ceil(Math.sqrt(count)); var rows = ((columns * columns) - count >= columns ? columns - 1 : columns); var padding = 12; var startX = padding; var startY = Page.startY; var totalWidth = window.innerWidth - startX; var totalHeight = window.innerHeight - startY; var box = new Rect(startX, startY, (totalWidth / columns) - padding, (totalHeight / rows) - padding); $.each(this.groups, function(index, group) { group.setBounds(box, true); box.left += box.width + padding; if(index % columns == columns - 1) { box.left = startX; box.top += box.height + padding; } }); }, // ---------- removeAll: function() { var toRemove = $.merge([], this.groups); $.each(toRemove, function(index, group) { group.removeAll(); }); }, // ---------- newTab: function(tabItem) { var group = this.getActiveGroup(); if( group == null ) group = this.getNewTabGroup(); var $el = $(tabItem.container); if(group) group.add($el); }, // ---------- // Function: getActiveGroup // Returns the active group. Active means the group where a new // tab will live when it is created as well as what tabs are // shown in the tab bar when not in the TabCandy interface. getActiveGroup: function() { return this._activeGroup; }, // ---------- // Function: setActiveGroup // Sets the active group, thereby showing only the relavent tabs // to that group. The change is visible only if the tab bar is // visible. // // Paramaters // group - the active or if no group is active // (which means we have an orphaned tab selected) setActiveGroup: function(group) { this._activeGroup = group; if(group) UI.tabBar.showOnlyTheseTabs( group._children ); else if( group == null) UI.tabBar.showOnlyTheseTabs( this.getOrphanedTabs() ); }, // ---------- // Function: getOrphanedTabs // Returns an array of all tabs that aren't in a group getOrphanedTabs: function(){ var tabs = TabItems.getItems(); tabs = tabs.filter(function(tab){ return tab.parent == null; }) return tabs } }; // ---------- Groups.init(); // ########## $(".tab").data('isDragging', false) .draggable(window.Groups.dragOptions) .droppable(window.Groups.dropOptions); })();