blob: c12a7e521bb4f12c60faf9b30b9058e3b1e2c30c [file] [log] [blame]
/*
* 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;