/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.NavigationBar = function(element, navigationItems, role, label) { WebInspector.Object.call(this); this._element = element || document.createElement("div"); this._element.classList.add(this.constructor.StyleClassName || WebInspector.NavigationBar.StyleClassName); this._element.tabIndex = 0; if (role) this._element.setAttribute("role", role); if (label) this._element.setAttribute("aria-label", label); this._element.addEventListener("focus", this._focus.bind(this), false); this._element.addEventListener("blur", this._blur.bind(this), false); this._element.addEventListener("keydown", this._keyDown.bind(this), false); this._element.addEventListener("mousedown", this._mouseDown.bind(this), false); document.addEventListener("load", this.updateLayout.bind(this), false); this._styleElement = document.createElement("style"); this._navigationItems = []; if (navigationItems) { for (var i = 0; i < navigationItems.length; ++i) this.addNavigationItem(navigationItems[i]); } document.head.appendChild(this._styleElement); }; WebInspector.Object.addConstructorFunctions(WebInspector.NavigationBar); WebInspector.NavigationBar.StyleClassName = "navigation-bar"; WebInspector.NavigationBar.CollapsedStyleClassName = "collapsed"; WebInspector.NavigationBar.Event = { NavigationItemSelected: "navigation-bar-navigation-item-selected" }; WebInspector.NavigationBar.prototype = { constructor: WebInspector.NavigationBar, // Public addNavigationItem: function(navigationItem, parentElement) { return this.insertNavigationItem(navigationItem, this._navigationItems.length, parentElement); }, insertNavigationItem: function(navigationItem, index, parentElement) { console.assert(navigationItem instanceof WebInspector.NavigationItem); if (!(navigationItem instanceof WebInspector.NavigationItem)) return; if (navigationItem.parentNavigationBar) navigationItem.parentNavigationBar.removeNavigationItem(navigationItem); navigationItem._parentNavigationBar = this; console.assert(index >= 0 && index <= this._navigationItems.length); index = Math.max(0, Math.min(index, this._navigationItems.length)); this._navigationItems.splice(index, 0, navigationItem); if (!parentElement) parentElement = this._element; var nextSibling = this._navigationItems[index + 1]; var nextSiblingElement = nextSibling ? nextSibling.element : null; if (nextSiblingElement && nextSiblingElement.parentNode !== parentElement) nextSiblingElement = null; parentElement.insertBefore(navigationItem.element, nextSiblingElement); this._minimumWidthNeedsRecalculation = true; this._needsStyleUpdated = true; this.updateLayoutSoon(); return navigationItem; }, removeNavigationItem: function(navigationItemOrIdentifierOrIndex, index) { var navigationItem = this._findNavigationItem(navigationItemOrIdentifierOrIndex); if (!navigationItem) return; navigationItem._parentNavigationBar = null; if (this._selectedNavigationItem === navigationItem) this.selectedNavigationItem = null; this._navigationItems.remove(navigationItem); navigationItem.element.remove(); this._minimumWidthNeedsRecalculation = true; this._needsStyleUpdated = true; this.updateLayoutSoon(); return navigationItem; }, updateLayoutSoon: function() { if (this._updateLayoutTimeout) return; this._needsLayout = true; function update() { delete this._updateLayoutTimeout; if (this._needsLayout || this._needsStyleUpdated) this.updateLayout(); } this._updateLayoutTimeout = setTimeout(update.bind(this), 0); }, updateLayout: function() { if (this._updateLayoutTimeout) { clearTimeout(this._updateLayoutTimeout); delete this._updateLayoutTimeout; } if (this._needsStyleUpdated) this._updateStyle(); this._needsLayout = false; if (typeof this.customUpdateLayout === "function") { this.customUpdateLayout(); return; } // Remove the collapsed style class to test if the items can fit at full width. this._element.classList.remove(WebInspector.NavigationBar.CollapsedStyleClassName); // Tell each navigation item to update to full width if needed. for (var i = 0; i < this._navigationItems.length; ++i) this._navigationItems[i].updateLayout(true); var totalItemWidth = 0; for (var i = 0; i < this._navigationItems.length; ++i) { // Skip flexible space items since they can take up no space at the minimum width. if (this._navigationItems[i] instanceof WebInspector.FlexibleSpaceNavigationItem) continue; totalItemWidth += this._navigationItems[i].element.offsetWidth; } var barWidth = this._element.offsetWidth; // Add the collapsed class back if the items are wider than the bar. if (totalItemWidth > barWidth) this._element.classList.add(WebInspector.NavigationBar.CollapsedStyleClassName); // Give each navigation item the opportunity to collapse further. for (var i = 0; i < this._navigationItems.length; ++i) this._navigationItems[i].updateLayout(); }, get selectedNavigationItem() { return this._selectedNavigationItem || null; }, set selectedNavigationItem(navigationItemOrIdentifierOrIndex) { var navigationItem = this._findNavigationItem(navigationItemOrIdentifierOrIndex); // Only radio navigation items can be selected. if (!(navigationItem instanceof WebInspector.RadioButtonNavigationItem)) navigationItem = null; if (this._selectedNavigationItem === navigationItem) return; if (this._selectedNavigationItem) this._selectedNavigationItem.selected = false; this._selectedNavigationItem = navigationItem || null; if (this._selectedNavigationItem) this._selectedNavigationItem.selected = true; // When the mouse is down don't dispatch the selected event, it will be dispatched on mouse up. // This prevents sending the event while the user is scrubbing the bar. if (!this._mouseIsDown) this.dispatchEventToListeners(WebInspector.NavigationBar.Event.NavigationItemSelected); }, get navigationItems() { return this._navigationItems; }, get element() { return this._element; }, get minimumWidth() { if (typeof this._minimumWidth === "undefined" || this._minimumWidthNeedsRecalculation) { this._minimumWidth = this._calculateMinimumWidth(); delete this._minimumWidthNeedsRecalculation; } return this._minimumWidth; }, get sizesToFit() { // Can be overriden by subclasses. return false; }, // Private _findNavigationItem: function(navigationItemOrIdentifierOrIndex) { var navigationItem = null; if (navigationItemOrIdentifierOrIndex instanceof WebInspector.NavigationItem) { if (this._navigationItems.contains(navigationItemOrIdentifierOrIndex)) navigationItem = navigationItemOrIdentifierOrIndex; } else if (typeof navigationItemOrIdentifierOrIndex === "number") { navigationItem = this._navigationItems[navigationItemOrIdentifierOrIndex]; } else if (typeof navigationItemOrIdentifierOrIndex === "string") { for (var i = 0; i < this._navigationItems.length; ++i) { if (this._navigationItems[i].identifier === navigationItemOrIdentifierOrIndex) { navigationItem = this._navigationItems[i]; break; } } } return navigationItem; }, _mouseDown: function(event) { // Only handle left mouse clicks. if (event.button !== 0) return; // Remove the tabIndex so clicking the navigation bar does not give it focus. // Only keep the tabIndex if already focused from keyboard navigation. This matches Xcode. if (!this._focused) this._element.removeAttribute("tabindex"); var itemElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.RadioButtonNavigationItem.StyleClassName); if (!itemElement || !itemElement.navigationItem) return; this._previousSelectedNavigationItem = this.selectedNavigationItem; this.selectedNavigationItem = itemElement.navigationItem; this._mouseIsDown = true; this._mouseMovedEventListener = this._mouseMoved.bind(this); this._mouseUpEventListener = this._mouseUp.bind(this); // Register these listeners on the document so we can track the mouse if it leaves the resizer. document.addEventListener("mousemove", this._mouseMovedEventListener, false); document.addEventListener("mouseup", this._mouseUpEventListener, false); event.preventDefault(); event.stopPropagation(); }, _mouseMoved: function(event) { console.assert(event.button === 0); console.assert(this._mouseIsDown); if (!this._mouseIsDown) return; event.preventDefault(); event.stopPropagation(); var itemElement = event.target.enclosingNodeOrSelfWithClass(WebInspector.RadioButtonNavigationItem.StyleClassName); if (!itemElement || !itemElement.navigationItem) { // Find the element that is at the X position of the mouse, even when the mouse is no longer // vertically in the navigation bar. var element = document.elementFromPoint(event.pageX, this._element.totalOffsetTop); if (!element) return; itemElement = element.enclosingNodeOrSelfWithClass(WebInspector.RadioButtonNavigationItem.StyleClassName); if (!itemElement || !itemElement.navigationItem) return; } if (this.selectedNavigationItem) this.selectedNavigationItem.active = false; this.selectedNavigationItem = itemElement.navigationItem; this.selectedNavigationItem.active = true; }, _mouseUp: function(event) { console.assert(event.button === 0); console.assert(this._mouseIsDown); if (!this._mouseIsDown) return; if (this.selectedNavigationItem) this.selectedNavigationItem.active = false; this._mouseIsDown = false; document.removeEventListener("mousemove", this._mouseMovedEventListener, false); document.removeEventListener("mouseup", this._mouseUpEventListener, false); delete this._mouseMovedEventListener; delete this._mouseUpEventListener; // Restore the tabIndex so the navigation bar can be in the keyboard tab loop. this._element.tabIndex = 0; // Dispatch the selected event here since the selectedNavigationItem setter surpresses it // while the mouse is down to prevent sending it while scrubbing the bar. if (this._previousSelectedNavigationItem !== this.selectedNavigationItem) this.dispatchEventToListeners(WebInspector.NavigationBar.Event.NavigationItemSelected); delete this._previousSelectedNavigationItem; event.preventDefault(); event.stopPropagation(); }, _keyDown: function(event) { if (!this._focused) return; if (event.keyIdentifier !== "Left" && event.keyIdentifier !== "Right") return; event.preventDefault(); event.stopPropagation(); var selectedNavigationItemIndex = this._navigationItems.indexOf(this._selectedNavigationItem); if (event.keyIdentifier === "Left") { if (selectedNavigationItemIndex === -1) selectedNavigationItemIndex = this._navigationItems.length; do { selectedNavigationItemIndex = Math.max(0, selectedNavigationItemIndex - 1); } while (selectedNavigationItemIndex && !(this._navigationItems[selectedNavigationItemIndex] instanceof WebInspector.RadioButtonNavigationItem)); } else if (event.keyIdentifier === "Right") { do { selectedNavigationItemIndex = Math.min(selectedNavigationItemIndex + 1, this._navigationItems.length - 1); } while (selectedNavigationItemIndex < this._navigationItems.length - 1 && !(this._navigationItems[selectedNavigationItemIndex] instanceof WebInspector.RadioButtonNavigationItem)); } if (!(this._navigationItems[selectedNavigationItemIndex] instanceof WebInspector.RadioButtonNavigationItem)) return; this.selectedNavigationItem = this._navigationItems[selectedNavigationItemIndex]; }, _focus: function(event) { this._focused = true; }, _blur: function(event) { this._focused = false; }, _updateStyle: function() { this._needsStyleUpdated = false; var parentSelector = "." + (this.constructor.StyleClassName || WebInspector.NavigationBar.StyleClassName); var styleText = ""; for (var i = 0; i < this._navigationItems.length; ++i) { if (!this._navigationItems[i].generateStyleText) continue; if (styleText) styleText += "\n"; styleText += this._navigationItems[i].generateStyleText(parentSelector); } this._styleElement.textContent = styleText; }, _calculateMinimumWidth: function() { var wasCollapsed = this._element.classList.contains(WebInspector.NavigationBar.CollapsedStyleClassName); // Add the collapsed style class to calculate the width of the items when they are collapsed. if (!wasCollapsed) this._element.classList.add(WebInspector.NavigationBar.CollapsedStyleClassName); var totalItemWidth = 0; for (var i = 0; i < this._navigationItems.length; ++i) { // Skip flexible space items since they can take up no space at the minimum width. if (this._navigationItems[i] instanceof WebInspector.FlexibleSpaceNavigationItem) continue; totalItemWidth += this._navigationItems[i].element.offsetWidth; } // Remove the collapsed style class if we were not collapsed before. if (!wasCollapsed) this._element.classList.remove(WebInspector.NavigationBar.CollapsedStyleClassName); return totalItemWidth; } }; WebInspector.NavigationBar.prototype.__proto__ = WebInspector.Object.prototype;