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..eaa42b02ea2033747064a217b42128c4893ad00f 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,67 @@ |
| // found in the LICENSE file. |
| /** |
| * @implements {SDK.SDKModelObserver<!SDK.RuntimeModel>} |
| + * @implements {UI.ListDelegate<!SDK.ExecutionContext>} |
| * @unrestricted |
| */ |
| -Console.ConsoleContextSelector = class { |
| - /** |
| - * @param {!Element} selectElement |
| - */ |
| - constructor(selectElement) { |
| - this._selectElement = selectElement; |
| - /** |
| - * @type {!Map.<!SDK.ExecutionContext, !Element>} |
| - */ |
| - this._optionByExecutionContext = new Map(); |
| +Console.ConsoleContextSelector = class extends UI.ToolbarItem { |
|
dgozman
2017/05/08 23:16:47
Should not extend from UI controls.
einbinder
2017/05/09 22:00:43
Done.
|
| + constructor() { |
| + super(createElementWithClass('button', 'console-context')); |
| + this._titleElement = this.element.createChild('span'); |
| + this._titleElement.textContent = ''; |
| + this._titleElement.style.paddingRight = '5px'; |
| + this._titleElement.style.width = '120px'; |
|
dgozman
2017/05/08 23:16:41
Remove?
einbinder
2017/05/09 22:00:45
Done.
|
| + this._titleElement.style.overflow = 'hidden'; |
| + this._titleElement.style.textOverflow = 'ellipsis '; |
| + this._productRegistry = null; |
| + |
| + ProductRegistry.instance().then(registry => { |
| + this._productRegistry = registry; |
| + this._list.refreshAllItems(); |
| + }); |
| + this.element.classList.add('toolbar-has-dropdown'); |
| + this.element.tabIndex = 0; |
| + this._glassPane = new UI.GlassPane(); |
| + this._glassPane.setMaxContentSize(new UI.Size(315, 310)); |
| + this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin); |
| + this._glassPane.setSizeBehavior(UI.GlassPane.SizeBehavior.SetExactWidthMaxHeight); |
| + this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom); |
| + this._glassPane.setOutsideClickCallback(this._hide.bind(this)); |
| + /** @type {!Array<!SDK.ExecutionContext>} */ |
| + this._executionContexts = []; |
|
dgozman
2017/05/08 23:16:42
You don't need this: list.itemAtIndex()
einbinder
2017/05/09 22:00:45
Done.
|
| + this._list = new UI.ListControl(this, UI.ListMode.NonViewport); |
|
dgozman
2017/05/08 23:16:47
Viewport it.
einbinder
2017/05/09 22:00:44
Viewporting it ended up being a lot slower.
dgozman
2017/05/09 23:11:12
Can you elaborate on this?
einbinder
2017/05/11 00:07:03
Fixed.
|
| + this._list.element.classList.add('context-list'); |
| + this._list.element.tabIndex = -1; |
| + var shadowRoot = |
| + UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'console/consoleContextSelector.css'); |
| + shadowRoot.appendChild(this._list.element); |
| + |
| + this._canSelect = false; |
|
dgozman
2017/05/08 23:16:44
shouldHideOnMouseUp
einbinder
2017/05/09 22:00:44
Done.
|
| + this.element.addEventListener('mousedown', event => { |
| + if (this._canSelect) |
| + this._hide(event); |
| + else |
| + this._show(event); |
| + }, false); |
| + this.element.addEventListener('click', event => { |
| + if (!this._canSelect) |
| + this._show(event); |
| + }, false); |
| + this.element.addEventListener('keydown', this._onKeyDown.bind(this), false); |
| + this.element.addEventListener('blur', this._hide.bind(this), false); |
| + this._list.element.addEventListener('mousedown', event => { |
| + this._hide(event); |
| + this._updateSelectedContext(); |
| + }, false); |
| + this._list.element.addEventListener('mouseup', event => { |
| + if (!this._canSelect) |
| + return; |
| + this._hide(event); |
| + this._updateSelectedContext(); |
| + }, false); |
| + |
| + var dropdownArrowIcon = UI.Icon.create('smallicon-triangle-down', 'toolbar-dropdown-arrow'); |
| + this.element.appendChild(dropdownArrowIcon); |
| SDK.targetManager.addModelListener( |
| SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextCreated, this._onExecutionContextCreated, this); |
| @@ -22,8 +71,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); |
| @@ -32,27 +82,139 @@ Console.ConsoleContextSelector = class { |
| } |
| /** |
| + * @param {!Event} event |
| + */ |
| + _show(event) { |
| + if (this._glassPane.isShowing()) |
| + return; |
| + this._glassPane.setContentAnchorBox(this.element.boxInWindow()); |
| + if (this.element.ownerDocument) |
| + this._glassPane.show(this.element.ownerDocument); |
|
dgozman
2017/05/08 23:16:42
nit: just cast it
einbinder
2017/05/09 22:00:43
Done.
|
| + this._list.viewportResized(); |
| + var selectedItem = this._list.selectedItem(); |
| + if (selectedItem) |
| + this._list.scrollItemIntoView(selectedItem, true); |
| + this.element.focus(); |
| + event.consume(true); |
| + setTimeout(() => this._canSelect = true, 200); |
|
dgozman
2017/05/08 23:16:45
Is it possible to put this in closure?
dgozman
2017/05/09 23:11:12
What about this?
einbinder
2017/05/11 00:07:03
It needs to be set to false if hidden by keyboard,
|
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| + */ |
| + _hide(event) { |
| + setTimeout(() => this._canSelect = false, 200); |
|
dgozman
2017/05/08 23:16:47
this._canSelect = false ?
einbinder
2017/05/09 22:00:45
If you click the button after the list is open, th
|
| + this._glassPane.hide(); |
| + SDK.OverlayModel.hideDOMNodeHighlight(); |
| + 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._executionContexts[this._executionContexts.indexOf(currentExecutionContext) + 1]; |
|
dgozman
2017/05/08 23:16:40
Let's expose selectedIndex()
einbinder
2017/05/09 22:00:44
Done.
|
| + 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._executionContexts.indexOf(currentExecutionContext) - 1; i >= 0; i--) { |
| + if (this._depthFor(this._executionContexts[i]) < depth) { |
| + handled = true; |
| + this._list.selectItem(this._executionContexts[i], false); |
| + break; |
| + } |
| + } |
| + break; |
| + case 'PageUp': |
| + handled = this._list.selectItemPreviousPage(false); |
| + break; |
| + case 'PageDown': |
| + handled = this._list.selectItemNextPage(false); |
| + break; |
| + case 'Escape': |
| + if (this._glassPane.isShowing()) |
| + event.consume(true); |
| + this._glassPane.hide(); |
| + return; |
| + case 'Enter': |
| + handled = this._glassPane.isShowing(); |
| + this._glassPane.hide(); |
| + break; |
| + default: |
| + if (event.key.length === 1) { |
| + var selectedItem = this._list.selectedItem(); |
| + var selectedIndex = selectedItem ? this._executionContexts.indexOf(selectedItem) : -1; |
| + var context = |
| + this._executionContexts.slice(selectedIndex + 1) |
| + .find(context => this._titleFor(context).toUpperCase().startsWith(event.key.toUpperCase())) || |
| + this._executionContexts.slice(0, selectedIndex) |
| + .find(context => this._titleFor(context).toUpperCase().startsWith(event.key.toUpperCase())); |
| + if (context) |
| + this._list.selectItem(context); |
| + 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 +226,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 +239,14 @@ 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; |
| + var index = this._executionContexts.lowerBound( |
| + executionContext, executionContext.runtimeModel.executionContextComparator()); |
| + this._executionContexts.splice(index, 0, executionContext); |
| + this._list.insertItemAtIndex(index, executionContext); |
|
dgozman
2017/05/08 23:16:42
Let's pass optional comparator to this method.
einbinder
2017/05/09 22:00:43
Done.
|
| + |
| + if (executionContext === UI.context.flavor(SDK.ExecutionContext)) { |
| + this._list.selectItem(executionContext); |
| + this._updateSelectedContext(); |
| } |
| } |
| @@ -116,9 +264,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._executionContexts.indexOf(executionContext) === -1) |
|
dgozman
2017/05/08 23:16:44
Let's fix this if it ever happens.
einbinder
2017/05/09 22:00:45
This is a hard fix. There are multiple listeners t
|
| + return; |
| + this._executionContextDestroyed(executionContext); |
| + this._executionContextCreated(executionContext); |
| this._updateSelectionWarning(); |
| } |
| @@ -126,8 +275,8 @@ Console.ConsoleContextSelector = class { |
| * @param {!SDK.ExecutionContext} executionContext |
| */ |
| _executionContextDestroyed(executionContext) { |
| - var option = this._optionByExecutionContext.remove(executionContext); |
| - option.remove(); |
| + if (this._executionContexts.remove(executionContext, true)) |
| + this._list.removeItem(executionContext); |
| } |
| /** |
| @@ -144,27 +293,15 @@ Console.ConsoleContextSelector = class { |
| */ |
| _executionContextChangedExternally(event) { |
| var executionContext = /** @type {?SDK.ExecutionContext} */ (event.data); |
| - if (!executionContext) |
| + if (!executionContext || this._executionContexts.indexOf(executionContext) === -1) |
|
dgozman
2017/05/08 23:16:42
ditto
|
| 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( |
| - 'warning', !this._isTopContext(executionContext) && this._hasTopContext()); |
| + this.element.classList.toggle('warning', !this._isTopContext(executionContext) && this._hasTopContext()); |
| } |
| /** |
| @@ -185,12 +322,7 @@ 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)) |
| - return true; |
| - } |
| - return false; |
| + return !!this._executionContexts.find(executionContext => this._isTopContext(executionContext)); |
| } |
| /** |
| @@ -206,50 +338,128 @@ 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._executionContexts.length; ++i) { |
| + if (this._executionContexts[i].runtimeModel === runtimeModel) |
| + this._executionContextDestroyed(this._executionContexts[i]); |
| } |
| } |
| /** |
| - * @param {!Element} option |
| + * @param {!SDK.ExecutionContext} executionContext |
| */ |
| - _select(option) { |
| - this._selectElement.selectedIndex = Array.prototype.indexOf.call(/** @type {?} */ (this._selectElement), option); |
| - this._updateSelectionWarning(); |
| + _onSelect(executionContext) { |
|
dgozman
2017/05/08 23:16:44
remove
einbinder
2017/05/09 22:00:44
Done.
|
| } |
| /** |
| - * @return {?Element} |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {!Element} |
| */ |
| - _selectedOption() { |
| - if (this._selectElement.selectedIndex >= 0) |
| - return this._selectElement[this._selectElement.selectedIndex]; |
| - return null; |
| + createElementForItem(item) { |
| + var element = createElementWithClass('div', 'context'); |
| + element.style.paddingLeft = (8 + this._depthFor(item) * 15) + 'px'; |
| + element.createChild('div', 'title').textContent = this._titleFor(item); |
| + element.createChild('div', 'subtitle').textContent = this._subtitleFor(item); |
| + element.addEventListener('mousemove', e => { |
| + if (e.movementX || e.movementY) |
| + this._list.selectItem(item, false, /* Don't scroll */ true); |
| + }); |
| + element.classList.toggle('disabled', !this.isItemSelectable(item)); |
| + return element; |
| } |
| /** |
| - * @param {!Common.Event} event |
| + * @param {!SDK.ExecutionContext} executionContext |
| + * @return {string} |
| */ |
| - _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]); |
| + _subtitleFor(executionContext) { |
|
dgozman
2017/05/08 23:16:44
Should we move this into some component maybe? Let
einbinder
2017/05/09 22:00:43
Moving it anywhere else is tricky because of depen
|
| + 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 {!Element} option |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {number} |
| */ |
| - _updateOptionDisabledState(option) { |
| - var executionContext = option.__executionContext; |
| - var callFrame = executionContext.debuggerModel.selectedCallFrame(); |
| + heightForItem(item) { |
| + return 20; |
|
dgozman
2017/05/08 23:16:41
Let the list measure.
einbinder
2017/05/09 22:00:44
Done.
|
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!SDK.ExecutionContext} item |
| + * @return {boolean} |
| + */ |
| + 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 +468,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._executionContexts.length; i++) { |
| + if (this._executionContexts[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._executionContexts.length; i++) { |
| + if (frameId === this._executionContexts[i].frameId) |
| + this._list.refreshItemsInRange(i, i + 1); |
| + } |
| + } |
| }; |