Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/console/ConsoleContextSelector.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/console/ConsoleContextSelector.js b/third_party/WebKit/Source/devtools/front_end/console/ConsoleContextSelector.js |
| index 127d7cee01d324c80b26e766a40696f8690f824c..1aa12bbd98553d6e48fed8e97528ac2cb0da77ff 100644 |
| --- a/third_party/WebKit/Source/devtools/front_end/console/ConsoleContextSelector.js |
| +++ b/third_party/WebKit/Source/devtools/front_end/console/ConsoleContextSelector.js |
| @@ -3,18 +3,58 @@ |
| // found in the LICENSE file. |
| /** |
| * @implements {SDK.SDKModelObserver<!SDK.RuntimeModel>} |
| - * @unrestricted |
| + * @implements {UI.ListDelegate<!SDK.ExecutionContext>} |
| */ |
| Console.ConsoleContextSelector = class { |
| - /** |
| - * @param {!Element} selectElement |
| - */ |
| - constructor(selectElement) { |
| - this._selectElement = selectElement; |
| - /** |
| - * @type {!Map.<!SDK.ExecutionContext, !Element>} |
| - */ |
| - this._optionByExecutionContext = new Map(); |
| + constructor() { |
| + this._toolbarItem = new UI.ToolbarItem(createElementWithClass('button', 'console-context')); |
| + var shadowRoot = |
| + UI.createShadowRootWithCoreStyles(this._toolbarItem.element, 'console/consoleContextSelectorButton.css'); |
| + this._titleElement = shadowRoot.createChild('span', 'title'); |
| + this._productRegistry = null; |
| + |
| + ProductRegistry.instance().then(registry => { |
| + this._productRegistry = registry; |
| + this._list.refreshAllItems(); |
| + }); |
| + this._toolbarItem.element.classList.add('toolbar-has-dropdown'); |
| + this._toolbarItem.element.tabIndex = 0; |
| + this._glassPane = new UI.GlassPane(); |
| + this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin); |
| + this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom); |
| + this._glassPane.setOutsideClickCallback(this._hide.bind(this)); |
| + this._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane); |
| + this._list = new UI.ListControl(this, UI.ListMode.EqualHeightItems); |
| + this._list.element.classList.add('context-list'); |
| + this._list.element.tabIndex = -1; |
| + this._rowHeight = 34; |
| + UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'console/consoleContextSelector.css') |
| + .appendChild(this._list.element); |
| + |
| + this._shouldHideOnMouseUp = false; |
| + this._toolbarItem.element.addEventListener('mousedown', event => { |
| + if (this._shouldHideOnMouseUp) |
| + this._hide(event); |
| + else |
| + this._show(event); |
| + }, false); |
| + this._toolbarItem.element.addEventListener('keydown', this._onKeyDown.bind(this), false); |
| + this._toolbarItem.element.addEventListener('focusout', this._hide.bind(this), false); |
| + this._list.element.addEventListener('mousedown', event => { |
| + event.consume(true); |
| + }, false); |
| + this._list.element.addEventListener('mouseup', event => { |
| + if (event.target === this._list.element) |
| + return; |
| + |
| + if (!this._shouldHideOnMouseUp) |
| + return; |
| + this._updateSelectedContext(); |
| + this._hide(event); |
| + }, false); |
| + |
| + var dropdownArrowIcon = UI.Icon.create('smallicon-triangle-down'); |
| + shadowRoot.appendChild(dropdownArrowIcon); |
| SDK.targetManager.addModelListener( |
| SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextCreated, this._onExecutionContextCreated, this); |
| @@ -22,8 +62,9 @@ Console.ConsoleContextSelector = class { |
| SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextChanged, this._onExecutionContextChanged, this); |
| SDK.targetManager.addModelListener( |
| SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextDestroyed, this._onExecutionContextDestroyed, this); |
| + SDK.targetManager.addModelListener( |
| + SDK.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated, this._frameNavigated, this); |
| - this._selectElement.addEventListener('change', this._executionContextChanged.bind(this), false); |
| UI.context.addFlavorChangeListener(SDK.ExecutionContext, this._executionContextChangedExternally, this); |
| UI.context.addFlavorChangeListener(SDK.DebuggerModel.CallFrame, this._callFrameSelectedInUI, this); |
| SDK.targetManager.observeModels(SDK.RuntimeModel, this); |
| @@ -31,28 +72,183 @@ Console.ConsoleContextSelector = class { |
| SDK.DebuggerModel, SDK.DebuggerModel.Events.CallFrameSelected, this._callFrameSelectedInModel, this); |
| } |
| + /** |
| + * @return {!UI.ToolbarItem} |
| + */ |
| + toolbarItem() { |
| + return this._toolbarItem; |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _show(event) { |
| + if (this._glassPane.isShowing()) |
| + return; |
| + this._glassPane.setContentAnchorBox(this._toolbarItem.element.boxInWindow()); |
| + this._glassPane.show(/** @type {!Document} **/ (this._toolbarItem.element.ownerDocument)); |
| + this._updateGlasspaneSize(); |
| + var selectedItem = this._list.selectedItem(); |
| + if (selectedItem) |
| + this._list.scrollItemIntoView(selectedItem, true); |
| + this._toolbarItem.element.focus(); |
| + event.consume(true); |
| + setTimeout(() => this._shouldHideOnMouseUp = true, 200); |
| + } |
| + |
| + _updateGlasspaneSize() { |
| + var maxHeight = this._rowHeight * (Math.min(this._list.length(), 9)); |
| + this._glassPane.setMaxContentSize(new UI.Size(315, maxHeight)); |
| + this._list.viewportResized(); |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _hide(event) { |
| + setTimeout(() => this._shouldHideOnMouseUp = false, 200); |
| + this._glassPane.hide(); |
| + SDK.OverlayModel.hideDOMNodeHighlight(); |
| + var selectedContext = UI.context.flavor(SDK.ExecutionContext); |
| + if (selectedContext) |
| + this._list.selectItem(selectedContext); |
| + event.consume(true); |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _onKeyDown(event) { |
| + var handled = false; |
| + switch (event.key) { |
| + case 'ArrowUp': |
| + handled = this._list.selectPreviousItem(false, false); |
| + break; |
| + case 'ArrowDown': |
| + handled = this._list.selectNextItem(false, false); |
| + break; |
| + case 'ArrowRight': |
| + var currentExecutionContext = this._list.selectedItem(); |
| + if (!currentExecutionContext) |
| + break; |
| + var nextExecutionContext = this._list.itemAtIndex(this._list.selectedIndex() + 1); |
| + if (nextExecutionContext && this._depthFor(currentExecutionContext) < this._depthFor(nextExecutionContext)) |
| + handled = this._list.selectNextItem(false, false); |
| + break; |
| + case 'ArrowLeft': |
| + var currentExecutionContext = this._list.selectedItem(); |
| + if (!currentExecutionContext) |
| + break; |
| + var depth = this._depthFor(currentExecutionContext); |
| + for (var i = this._list.selectedIndex() - 1; i >= 0; i--) { |
| + if (this._depthFor(this._list.itemAtIndex(i)) < depth) { |
| + handled = true; |
| + this._list.selectItem(this._list.itemAtIndex(i), false); |
| + break; |
| + } |
| + } |
| + break; |
| + case 'PageUp': |
| + handled = this._list.selectItemPreviousPage(false); |
| + break; |
| + case 'PageDown': |
| + handled = this._list.selectItemNextPage(false); |
| + break; |
| + case 'Home': |
| + for (var i = 0; i < this._list.length(); i++) { |
| + if (this.isItemSelectable(this._list.itemAtIndex(i))) { |
| + this._list.selectItem(this._list.itemAtIndex(i)); |
| + handled = true; |
| + break; |
| + } |
| + } |
| + break; |
| + case 'End': |
| + for (var i = this._list.length() - 1; i >= 0; i--) { |
| + if (this.isItemSelectable(this._list.itemAtIndex(i))) { |
| + this._list.selectItem(this._list.itemAtIndex(i)); |
| + handled = true; |
| + break; |
| + } |
| + } |
| + break; |
| + case 'Escape': |
| + this._hide(event); |
| + break; |
| + case 'Tab': |
| + if (!this._glassPane.isShowing()) |
| + break; |
| + this._updateSelectedContext(); |
| + this._hide(event); |
| + break; |
| + case 'Enter': |
| + if (!this._glassPane.isShowing()) { |
| + this._show(event); |
| + break; |
| + } |
| + this._updateSelectedContext(); |
| + this._hide(event); |
| + break; |
| + case ' ': |
| + this._show(event); |
| + break; |
| + default: |
| + if (event.key.length === 1) { |
| + var selectedIndex = this._list.selectedIndex(); |
| + var letter = event.key.toUpperCase(); |
| + for (var i = 0; i < this._list.length(); i++) { |
| + var context = this._list.itemAtIndex((selectedIndex + i + 1) % this._list.length()); |
| + if (this._titleFor(context).toUpperCase().startsWith(letter)) { |
| + this._list.selectItem(context); |
| + break; |
| + } |
| + } |
| + handled = true; |
| + } |
| + break; |
| + } |
| + |
| + if (handled) { |
| + event.consume(true); |
| + this._updateSelectedContext(); |
| + } |
| + } |
| + |
| /** |
| * @param {!SDK.ExecutionContext} executionContext |
| * @return {string} |
| */ |
| _titleFor(executionContext) { |
| var target = executionContext.target(); |
| - var depth = 0; |
| var label = executionContext.label() ? target.decorateLabel(executionContext.label()) : ''; |
| + if (executionContext.frameId) { |
| + var resourceTreeModel = target.model(SDK.ResourceTreeModel); |
| + var frame = resourceTreeModel && resourceTreeModel.frameForId(executionContext.frameId); |
| + if (frame) |
| + label = label || frame.displayName(); |
| + } |
| + label = label || executionContext.origin; |
| + |
| + return label; |
| + } |
| + |
| + /** |
| + * @param {!SDK.ExecutionContext} executionContext |
| + * @return {number} |
| + */ |
| + _depthFor(executionContext) { |
| + var target = executionContext.target(); |
| + var depth = 0; |
| if (!executionContext.isDefault) |
| depth++; |
| if (executionContext.frameId) { |
| var resourceTreeModel = target.model(SDK.ResourceTreeModel); |
| var frame = resourceTreeModel && resourceTreeModel.frameForId(executionContext.frameId); |
| - if (frame) { |
| - label = label || frame.displayName(); |
| - while (frame.parentFrame) { |
| - depth++; |
| - frame = frame.parentFrame; |
| - } |
| + while (frame && frame.parentFrame) { |
| + depth++; |
| + frame = frame.parentFrame; |
| } |
| } |
| - label = label || executionContext.origin; |
| var targetDepth = 0; |
| while (target.parentTarget()) { |
| if (target.parentTarget().hasJSCapability()) { |
| @@ -64,11 +260,8 @@ Console.ConsoleContextSelector = class { |
| } |
| target = target.parentTarget(); |
| } |
| - |
| depth += targetDepth; |
| - var prefix = new Array(4 * depth + 1).join('\u00a0'); |
| - var maxLength = 50; |
| - return (prefix + label).trimMiddle(maxLength); |
| + return depth; |
| } |
| /** |
| @@ -80,25 +273,12 @@ Console.ConsoleContextSelector = class { |
| if (!executionContext.target().hasJSCapability()) |
| return; |
| - var newOption = createElement('option'); |
| - newOption.__executionContext = executionContext; |
| - newOption.text = this._titleFor(executionContext); |
| - this._optionByExecutionContext.set(executionContext, newOption); |
| - var options = this._selectElement.options; |
| - var contexts = Array.prototype.map.call(options, mapping); |
| - var index = contexts.lowerBound(executionContext, executionContext.runtimeModel.executionContextComparator()); |
| - this._selectElement.insertBefore(newOption, options[index]); |
| - |
| - if (executionContext === UI.context.flavor(SDK.ExecutionContext)) |
| - this._select(newOption); |
| - this._updateOptionDisabledState(newOption); |
| - |
| - /** |
| - * @param {!Element} option |
| - * @return {!SDK.ExecutionContext} |
| - */ |
| - function mapping(option) { |
| - return option.__executionContext; |
| + this._list.insertItemWithComparator(executionContext, executionContext.runtimeModel.executionContextComparator()); |
| + |
| + this._updateDisabledState(); |
| + if (executionContext === UI.context.flavor(SDK.ExecutionContext)) { |
| + this._list.selectItem(executionContext); |
| + this._updateSelectedContext(); |
| } |
| } |
| @@ -109,6 +289,7 @@ Console.ConsoleContextSelector = class { |
| var executionContext = /** @type {!SDK.ExecutionContext} */ (event.data); |
| this._executionContextCreated(executionContext); |
| this._updateSelectionWarning(); |
| + this._updateGlasspaneSize(); |
| } |
| /** |
| @@ -116,9 +297,10 @@ Console.ConsoleContextSelector = class { |
| */ |
| _onExecutionContextChanged(event) { |
| var executionContext = /** @type {!SDK.ExecutionContext} */ (event.data); |
| - var option = this._optionByExecutionContext.get(executionContext); |
| - if (option) |
| - option.text = this._titleFor(executionContext); |
| + if (this._list.indexOfItem(executionContext) === -1) |
| + return; |
| + this._executionContextDestroyed(executionContext); |
| + this._executionContextCreated(executionContext); |
| this._updateSelectionWarning(); |
| } |
| @@ -126,8 +308,15 @@ Console.ConsoleContextSelector = class { |
| * @param {!SDK.ExecutionContext} executionContext |
| */ |
| _executionContextDestroyed(executionContext) { |
| - var option = this._optionByExecutionContext.remove(executionContext); |
| - option.remove(); |
| + if (this._list.indexOfItem(executionContext) === -1) |
| + return; |
| + this._list.removeItem(executionContext); |
| + this._updateDisabledState(); |
| + this._updateGlasspaneSize(); |
| + } |
| + |
| + _updateDisabledState() { |
| + this._toolbarItem.element.disabled = (this._list.length() <= 1); |
|
dgozman
2017/05/11 21:14:52
Let's land this in a separate patch.
einbinder
2017/05/11 21:47:22
Done.
|
| } |
| /** |
| @@ -144,26 +333,15 @@ Console.ConsoleContextSelector = class { |
| */ |
| _executionContextChangedExternally(event) { |
| var executionContext = /** @type {?SDK.ExecutionContext} */ (event.data); |
| - if (!executionContext) |
| + if (!executionContext || this._list.indexOfItem(executionContext) === -1) |
| return; |
| - |
| - var options = this._selectElement.options; |
| - for (var i = 0; i < options.length; ++i) { |
| - if (options[i].__executionContext === executionContext) |
| - this._select(options[i]); |
| - } |
| - } |
| - |
| - _executionContextChanged() { |
| - var option = this._selectedOption(); |
| - var newContext = option ? option.__executionContext : null; |
| - UI.context.setFlavor(SDK.ExecutionContext, newContext); |
| - this._updateSelectionWarning(); |
| + this._list.selectItem(executionContext); |
| + this._updateSelectedContext(); |
| } |
| _updateSelectionWarning() { |
| var executionContext = UI.context.flavor(SDK.ExecutionContext); |
| - this._selectElement.parentElement.classList.toggle( |
| + this._toolbarItem.element.classList.toggle( |
| 'warning', !this._isTopContext(executionContext) && this._hasTopContext()); |
| } |
| @@ -185,9 +363,8 @@ Console.ConsoleContextSelector = class { |
| * @return {boolean} |
| */ |
| _hasTopContext() { |
| - var options = this._selectElement.options; |
| - for (var i = 0; i < options.length; i++) { |
| - if (this._isTopContext(options[i].__executionContext)) |
| + for (var i = 0; i < this._list.length(); i++) { |
| + if (this._isTopContext(this._list.itemAtIndex(i))) |
| return true; |
| } |
| return false; |
| @@ -206,50 +383,122 @@ Console.ConsoleContextSelector = class { |
| * @param {!SDK.RuntimeModel} runtimeModel |
| */ |
| modelRemoved(runtimeModel) { |
| - var executionContexts = this._optionByExecutionContext.keysArray(); |
| - for (var i = 0; i < executionContexts.length; ++i) { |
| - if (executionContexts[i].runtimeModel === runtimeModel) |
| - this._executionContextDestroyed(executionContexts[i]); |
| + for (var i = 0; i < this._list.length(); i++) { |
| + if (this._list.itemAtIndex(i).runtimeModel === runtimeModel) |
| + this._executionContextDestroyed(this._list.itemAtIndex(i)); |
| } |
| } |
| /** |
| - * @param {!Element} option |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {!Element} |
| */ |
| - _select(option) { |
| - this._selectElement.selectedIndex = Array.prototype.indexOf.call(/** @type {?} */ (this._selectElement), option); |
| - this._updateSelectionWarning(); |
| + createElementForItem(item) { |
| + var element = createElementWithClass('div', 'context'); |
| + element.style.paddingLeft = (8 + this._depthFor(item) * 15) + 'px'; |
| + element.createChild('div', 'title').textContent = this._titleFor(item).trimEnd(100); |
| + element.createChild('div', 'subtitle').textContent = this._subtitleFor(item); |
| + element.addEventListener('mousemove', e => { |
| + if ((e.movementX || e.movementY) && this.isItemSelectable(item)) |
| + this._list.selectItem(item, false, /* Don't scroll */ true); |
| + }); |
| + element.classList.toggle('disabled', !this.isItemSelectable(item)); |
| + return element; |
| } |
| /** |
| - * @return {?Element} |
| + * @param {!SDK.ExecutionContext} executionContext |
| + * @return {string} |
| */ |
| - _selectedOption() { |
| - if (this._selectElement.selectedIndex >= 0) |
| - return this._selectElement[this._selectElement.selectedIndex]; |
| - return null; |
| + _subtitleFor(executionContext) { |
| + var target = executionContext.target(); |
| + if (executionContext.frameId) { |
| + var resourceTreeModel = target.model(SDK.ResourceTreeModel); |
| + var frame = resourceTreeModel && resourceTreeModel.frameForId(executionContext.frameId); |
| + } |
| + if (executionContext.origin.startsWith('chrome-extension://')) |
| + return Common.UIString('Extension'); |
| + if (!frame || !frame.parentFrame || frame.parentFrame.securityOrigin !== executionContext.origin) { |
| + var url = executionContext.origin.asParsedURL(); |
| + if (url) { |
| + if (this._productRegistry) { |
| + var product = this._productRegistry.nameForUrl(url); |
| + if (product) |
| + return product; |
| + } |
| + return url.domain(); |
| + } |
| + } |
| + |
| + if (frame) { |
| + var stackTrace = frame.creationStackTrace(); |
| + while (stackTrace) { |
| + for (var stack of stackTrace.callFrames) { |
| + if (stack.url) { |
| + var url = new Common.ParsedURL(stack.url); |
| + if (this._productRegistry) { |
| + var product = this._productRegistry.nameForUrl(url); |
| + if (product) |
| + return product; |
| + } |
| + return url.domain(); |
| + } |
| + } |
| + stackTrace = frame.parent; |
| + } |
| + } |
| + return ''; |
| } |
| /** |
| - * @param {!Common.Event} event |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {number} |
| */ |
| - _callFrameSelectedInModel(event) { |
| - var debuggerModel = /** @type {!SDK.DebuggerModel} */ (event.data); |
| - var options = this._selectElement.options; |
| - for (var i = 0; i < options.length; i++) { |
| - if (options[i].__executionContext.debuggerModel === debuggerModel) |
| - this._updateOptionDisabledState(options[i]); |
| - } |
| + heightForItem(item) { |
| + return 0; |
| } |
| /** |
| - * @param {!Element} option |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {boolean} |
| */ |
| - _updateOptionDisabledState(option) { |
| - var executionContext = option.__executionContext; |
| - var callFrame = executionContext.debuggerModel.selectedCallFrame(); |
| + isItemSelectable(item) { |
| + var callFrame = item.debuggerModel.selectedCallFrame(); |
| var callFrameContext = callFrame && callFrame.script.executionContext(); |
| - option.disabled = callFrameContext && executionContext !== callFrameContext; |
| + return !callFrameContext || item === callFrameContext; |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {?SDK.ExecutionContext} from |
| + * @param {?SDK.ExecutionContext} to |
| + * @param {?Element} fromElement |
| + * @param {?Element} toElement |
| + */ |
| + selectedItemChanged(from, to, fromElement, toElement) { |
| + if (fromElement) |
| + fromElement.classList.remove('selected'); |
| + if (toElement) |
| + toElement.classList.add('selected'); |
| + SDK.OverlayModel.hideDOMNodeHighlight(); |
| + if (to && to.frameId) { |
| + var resourceTreeModel = to.target().model(SDK.ResourceTreeModel); |
| + if (resourceTreeModel) |
| + resourceTreeModel.domModel().overlayModel().highlightFrame(to.frameId); |
| + } |
| + } |
| + |
| + _updateSelectedContext() { |
| + var context = this._list.selectedItem(); |
| + if (context) |
| + this._titleElement.textContent = this._titleFor(context); |
| + else |
| + this._titleElement.textContent = ''; |
| + UI.context.setFlavor(SDK.ExecutionContext, context); |
| + this._updateSelectionWarning(); |
| } |
| _callFrameSelectedInUI() { |
| @@ -258,4 +507,26 @@ Console.ConsoleContextSelector = class { |
| if (callFrameContext) |
| UI.context.setFlavor(SDK.ExecutionContext, callFrameContext); |
| } |
| + |
| + /** |
| + * @param {!Common.Event} event |
| + */ |
| + _callFrameSelectedInModel(event) { |
| + var debuggerModel = /** @type {!SDK.DebuggerModel} */ (event.data); |
| + for (var i = 0; i < this._list.length(); i++) { |
| + if (this._list.itemAtIndex(i).debuggerModel === debuggerModel) |
| + this._list.refreshItemsInRange(i, i + 1); |
| + } |
| + } |
| + |
| + /** |
| + * @param {!Common.Event} event |
| + */ |
| + _frameNavigated(event) { |
| + var frameId = event.data.id; |
| + for (var i = 0; i < this._list.length(); i++) { |
| + if (frameId === this._list.itemAtIndex(i).frameId) |
| + this._list.refreshItemsInRange(i, i + 1); |
| + } |
| + } |
| }; |