| /* |
| * 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; |