Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/resources/FrameSelector.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/resources/FrameSelector.js b/third_party/WebKit/Source/devtools/front_end/resources/FrameSelector.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f68b8a38108c3d15030d97c8e15a86f4811a255c |
| --- /dev/null |
| +++ b/third_party/WebKit/Source/devtools/front_end/resources/FrameSelector.js |
| @@ -0,0 +1,324 @@ |
| +// Copyright 2017 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +Resources.FrameSelector = class extends Common.Object { |
|
dgozman
2017/03/24 17:06:54
Let's think about reusing this in Console right no
eostroukhov
2017/04/04 23:47:17
Acknowledged.
|
| + constructor() { |
| + super(); |
| + /** @type {!Map<string, !Resources._FrameTreeElement>} */ |
| + this._treeElementForFrameId = new Map(); |
| + |
| + this._button = createElement('button'); |
| + this._button.className = 'frame-selector'; |
| + this._button.appendChild(UI.Icon.create('largeicon-navigator-frame')); |
| + this._nameLabel = this._button.createChild('span', 'frame-name'); |
| + this._button.appendChild(UI.Icon.create('smallicon-dropdown-arrow')); |
| + |
| + this._button.addEventListener('click', () => this._showGlassPane()); |
| + |
| + this._tree = new UI.TreeOutlineInShadow(); |
| + this._tree.element.classList.add('frame-selector-popup'); |
| + this._tree.element.classList.add('filter-all'); |
| + |
| + var eventTarget = this._tree.contentElement; |
| + eventTarget.addEventListener('click', () => this._commitSelection()); |
| + eventTarget.addEventListener('mouseleave', () => this._onmouseleave(), false); |
| + eventTarget.addEventListener('mousemove', event => this._onmousemove(event), false); |
| + eventTarget.addEventListener('keydown', event => this._keyDown(event)); |
| + |
| + this._button.ownerDocument.defaultView.addEventListener('blur', () => this._hideGlassPane(null)); |
| + |
| + this._glassPane = new UI.GlassPane(); |
| + this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom); |
| + this._glassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.SetExactWidthMaxHeight); |
| + this._glassPane.setSetOutsideClickCallback(event => this._hideGlassPane(/** @type {!Node} */ (event.target))); |
| + |
| + UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'resources/frameSelector.css') |
| + .appendChild(this._tree.element); |
| + |
| + // Suppress the popup once - e.g. because user clicked on the button while the popup is visible |
| + // which should not instantly reopen popup. |
| + this._suppressNextPopup = false; |
| + |
| + /** @type {?Resources._FrameTreeElement} */ |
| + this._previousHoveredElement = null; |
| + |
| + /** @type {?Element} */ |
| + this._storedFocus = null; |
| + |
| + /** @type {?SDK.ResourceTreeFrame} */ |
| + this._selectedFrame = null; |
| + |
| + /** @type {!Array<?Element>} */ |
| + this._focusedElementsStack = []; |
| + |
| + this._initialize(); |
| + } |
| + |
| + _initialize() { |
| + function addListener(eventType, handler, target) { |
|
dgozman
2017/03/24 17:06:54
Just spell it out 3 times - makes it easier to sea
eostroukhov
2017/04/04 23:47:17
Done, but I did that lambda so I don't have to rep
|
| + SDK.targetManager.addModelListener(SDK.ResourceTreeModel, eventType, event => handler.call(target, event.data)); |
| + } |
| + addListener(SDK.ResourceTreeModel.Events.FrameAdded, this.frameAdded, this); |
| + addListener(SDK.ResourceTreeModel.Events.FrameNavigated, this.frameNavigated, this); |
| + addListener(SDK.ResourceTreeModel.Events.FrameDetached, this.frameDetached, this); |
| + |
| + var mainTarget = SDK.targetManager.mainTarget(); |
|
dgozman
2017/03/24 17:06:54
Let's make this multitarget right away.
eostroukhov
2017/04/04 23:47:17
Please take a look.
|
| + var resourceTreeModel = mainTarget && mainTarget.hasDOMCapability() && SDK.ResourceTreeModel.fromTarget(mainTarget); |
|
dgozman
2017/03/24 17:06:54
You don't have to check capability anymore: mainTa
eostroukhov
2017/04/04 23:47:17
Done.
|
| + var mainFrame = resourceTreeModel.mainFrame; |
| + if (mainFrame) |
| + this.populateFrame(mainFrame); |
| + |
| + this.selectFrame(null); |
| + } |
| + |
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + * @returns {?SDK.ResourceTreeFrame} |
| + */ |
| + static _getParentFrame(frame) { |
| + var parentFrame = frame.parentFrame; |
| + if (parentFrame) |
| + return parentFrame; |
| + var parentTarget = frame.target().parentTarget(); |
| + while (parentTarget && !parentTarget.hasDOMCapability()) |
| + parentTarget = parentTarget.parentTarget(); |
| + if (!parentTarget) |
| + return null; |
| + var model = SDK.ResourceTreeModel.fromTarget(parentTarget); |
| + return model.mainFrame; |
| + } |
| + |
| + /** |
| + * @returns {!Element} |
| + */ |
| + get element() { |
| + return this._button; |
| + } |
| + |
| + /** |
| + * @param {!Element} root |
| + * @return {function()} Call to remove the listener |
| + */ |
| + trackFocus(root) { |
| + var listener = (event => { |
| + this._focusedElementsStack = [event.target.ownerDocument.deepActiveElement(), this._focusedElementsStack[0]]; |
| + }); |
| + root.element.addEventListener('focusin', listener); |
| + return function() { |
| + root.removeEventListener('focusin', listener); |
| + }; |
| + } |
| + |
| + /** |
| + * @return {?SDK.ResourceTreeFrame} frame |
| + */ |
| + selectedFrame() { |
| + return this._selectedFrame; |
| + } |
| + |
| + selectRootFrame() { |
| + var firstElement = this._tree.firstChild(); |
| + this.selectFrame(firstElement && firstElement.frame); |
| + } |
| + |
| + _showGlassPane() { |
| + var doc = this._button.ownerDocument; |
| + if (!doc || this._suppressNextPopup) { |
| + this._suppressNextPopup = false; |
| + return; |
| + } |
| + |
| + // Restored focus should not go to button but to previously focused component |
| + this._storedFocus = this._focusedElementsStack[0]; |
| + if (this._storedFocus === this._button) |
| + this._storedFocus = this._focusedElementsStack[1]; |
| + |
| + var anchorBox = this._button.boxInWindow(window); |
| + // Account for border |
| + anchorBox.x = 0; |
| + anchorBox.width -= 4; |
| + anchorBox.height -= 8; |
| + var width = Math.max(300, anchorBox.width); |
| + this._glassPane.setMaxContentSize(new UI.Size(width, 300)); |
| + // Position popup relative to arrow dowm and not just below the button |
| + // anchorBox.height = childBox.y + childBox.height - anchorBox.y; |
|
dgozman
2017/03/24 17:06:54
Commented code.
eostroukhov
2017/04/04 23:47:17
Oops. Fixed!
|
| + this._glassPane.setContentAnchorBox(anchorBox); |
| + this._glassPane.show(doc); |
| + this._tree.firstChild().expand(); |
| + this._tree.focus(); |
| + } |
| + |
| + /** |
| + * @param {?Node} targetNode |
| + */ |
| + _hideGlassPane(targetNode) { |
| + this._suppressNextPopup = this._button.isSelfOrAncestor(targetNode); |
| + this._glassPane.hide(); |
| + if (this._storedFocus) { |
| + this._storedFocus.focus(); |
| + this._storedFocus = null; |
| + } |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _keyDown(event) { |
| + if (!isEscKey(event) && !isEnterKey(event)) |
| + return; |
| + if (isEnterKey(event)) |
| + this._commitSelection(); |
| + this._hideGlassPane(null); |
| + event.consume(); |
| + } |
| + |
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + */ |
| + frameAdded(frame) { |
| + var parentFrame = Resources.FrameSelector._getParentFrame(frame); |
| + var parentTreeElement = parentFrame ? this._treeElementForFrameId.get(parentFrame.id) : this._tree; |
| + if (!parentTreeElement) { |
| + console.warn('No frame to route ' + frame.url + ' to.'); |
| + return; |
| + } |
| + |
| + var frameTreeElement = new Resources._FrameTreeElement(frame); |
| + this._treeElementForFrameId.set(frame.id, frameTreeElement); |
| + parentTreeElement.appendChild(frameTreeElement); |
| + } |
| + |
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + */ |
| + frameDetached(frame) { |
| + var frameTreeElement = this._treeElementForFrameId.get(frame.id); |
| + if (!frameTreeElement) |
| + return; |
| + |
| + this._treeElementForFrameId.remove(frame.id); |
| + if (frameTreeElement.parent) |
| + frameTreeElement.parent.removeChild(frameTreeElement); |
| + } |
| + |
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + */ |
| + frameNavigated(frame) { |
| + if (!Resources.FrameSelector._getParentFrame(frame)) |
| + return; |
| + var frameTreeElement = this._treeElementForFrameId.get(frame.id); |
| + if (frameTreeElement) |
| + frameTreeElement.frameNavigated(frame); |
| + } |
| + |
| + reset() { |
| + this._tree.removeChildren(); |
| + this._treeElementForFrameId.clear(); |
| + } |
| + |
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + */ |
| + populateFrame(frame) { |
| + this.frameAdded(frame); |
| + for (var child of frame.childFrames) |
| + this.populateFrame(child); |
| + } |
| + |
| + /** |
| + * @param {?SDK.ResourceTreeFrame} frame |
| + */ |
| + selectFrame(frame) { |
| + this._selectedFrame = frame; |
| + |
| + var displayName = frame ? frame.displayName() : ''; |
| + var frameName = frame ? frame.name : ''; |
| + |
| + this._nameLabel.textContent = frameName || displayName; |
| + this._button.title = displayName; |
| + this._hideGlassPane(null); |
| + } |
| + |
| + _commitSelection() { |
| + var selected = this._tree.selectedTreeElement; |
| + var frame = selected && selected.frame; |
| + this.selectFrame(frame); |
| + this.dispatchEventToListeners(Resources.FrameSelector.Events.FrameSelected, frame); |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _onmousemove(event) { |
| + var nodeUnderMouse = event.target; |
| + if (!nodeUnderMouse) |
| + return; |
| + |
| + var listNode = nodeUnderMouse.enclosingNodeOrSelfWithNodeName('li'); |
| + if (!listNode) |
| + return; |
| + |
| + var element = listNode.treeElement; |
| + if (this._previousHoveredElement === element) |
| + return; |
| + |
| + if (this._previousHoveredElement) { |
| + this._previousHoveredElement.hovered = false; |
| + this._previousHoveredElement = null; |
| + } |
| + |
| + if (element instanceof Resources._FrameTreeElement) { |
| + this._previousHoveredElement = element; |
| + element.select(false, false); |
| + element.hovered = true; |
| + } |
| + } |
| + |
| + _onmouseleave() { |
| + if (this._previousHoveredElement) { |
| + this._previousHoveredElement.hovered = false; |
| + this._previousHoveredElement = null; |
| + } |
| + } |
| +}; |
| + |
| +/** |
| + * @enum {symbol} |
| + */ |
| +Resources.FrameSelector.Events = { |
| + FrameSelected: Symbol('FrameSelected') |
| +}; |
| + |
| +Resources._FrameTreeElement = class extends UI.TreeElement { |
|
dgozman
2017/03/24 17:06:54
Why do we need a treeoutline? Let's reuse Filtered
eostroukhov
2017/04/04 23:47:17
Done.
|
| + /** |
| + * @param {!SDK.ResourceTreeFrame} frame |
| + */ |
| + constructor(frame) { |
| + super('', false); |
| + this.frame = frame; |
| + this.frameNavigated(frame); |
| + } |
| + |
| + frameNavigated(frame) { |
| + this.frame = frame; |
| + this.title = frame.displayName(); |
| + } |
| + |
| + get itemURL() { |
|
dgozman
2017/03/24 17:06:54
Unused.
eostroukhov
2017/04/04 23:47:17
Done.
|
| + return 'frame://' + encodeURI(this.titleAsText()); |
| + } |
| + |
| + set hovered(hovered) { |
| + if (hovered) { |
| + this.listItemElement.classList.add('hovered'); |
| + var domModel = SDK.DOMModel.fromTarget(this.frame.target()); |
| + if (domModel) |
| + domModel.highlightFrame(this.frame.id); |
| + } else { |
| + this.listItemElement.classList.remove('hovered'); |
| + SDK.DOMModel.hideDOMNodeHighlight(); |
| + } |
| + } |
| +}; |