UNPKG

chrome-devtools-frontend

Version:
361 lines (328 loc) 13.2 kB
// Copyright 2018 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. /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as ObjectUI from '../object_ui/object_ui.js'; import * as Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as UI from '../ui/ui.js'; export const UIStrings = { /** *@description A context menu item in the Console Pin Pane of the Console panel */ removeExpression: 'Remove expression', /** *@description A context menu item in the Console Pin Pane of the Console panel */ removeAllExpressions: 'Remove all expressions', /** *@description Screen reader label for delete button on a non-blank live expression *@example {document} PH1 */ removeExpressionS: 'Remove expression: {PH1}', /** *@description Screen reader label for delete button on a blank live expression */ removeBlankExpression: 'Remove blank expression', /** *@description Text in Console Pin Pane of the Console panel */ liveExpressionEditor: 'Live expression editor', /** *@description Text in Console Pin Pane of the Console panel */ expression: 'Expression', /** *@description Side effect label title in Console Pin Pane of the Console panel */ evaluateAllowingSideEffects: 'Evaluate, allowing side effects', /** *@description Text of a DOM element in Console Pin Pane of the Console panel */ notAvailable: 'not available', }; const str_ = i18n.i18n.registerUIStrings('console/ConsolePinPane.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const elementToConsolePin = new WeakMap<Element, ConsolePin>(); export class ConsolePinPane extends UI.ThrottledWidget.ThrottledWidget { _liveExpressionButton: UI.Toolbar.ToolbarButton; _pins: Set<ConsolePin>; _pinsSetting: Common.Settings.Setting<string[]>; constructor(liveExpressionButton: UI.Toolbar.ToolbarButton) { super(true, 250); this._liveExpressionButton = liveExpressionButton; this.registerRequiredCSS('console/consolePinPane.css', {enableLegacyPatching: true}); this.registerRequiredCSS('object_ui/objectValue.css', {enableLegacyPatching: true}); this.contentElement.classList.add('console-pins', 'monospace'); this.contentElement.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); this._pins = new Set(); this._pinsSetting = Common.Settings.Settings.instance().createLocalSetting('consolePins', []); for (const expression of this._pinsSetting.get()) { this.addPin(expression); } } willHide(): void { for (const pin of this._pins) { pin.setHovered(false); } } _savePins(): void { const toSave = Array.from(this._pins).map(pin => pin.expression()); this._pinsSetting.set(toSave); } _contextMenuEventFired(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); const target = UI.UIUtils.deepElementFromEvent(event); if (target) { const targetPinElement = target.enclosingNodeOrSelfWithClass('console-pin'); if (targetPinElement) { const targetPin = elementToConsolePin.get(targetPinElement); if (targetPin) { contextMenu.editSection().appendItem( i18nString(UIStrings.removeExpression), this._removePin.bind(this, targetPin)); targetPin.appendToContextMenu(contextMenu); } } } contextMenu.editSection().appendItem(i18nString(UIStrings.removeAllExpressions), this._removeAllPins.bind(this)); contextMenu.show(); } _removeAllPins(): void { for (const pin of this._pins) { this._removePin(pin); } } _removePin(pin: ConsolePin): void { pin.element().remove(); const newFocusedPin = this._focusedPinAfterDeletion(pin); this._pins.delete(pin); this._savePins(); if (newFocusedPin) { newFocusedPin.focus(); } else { this._liveExpressionButton.focus(); } } addPin(expression: string, userGesture?: boolean): void { const pin = new ConsolePin(expression, this); this.contentElement.appendChild(pin.element()); this._pins.add(pin); this._savePins(); if (userGesture) { pin.focus(); } this.update(); } _focusedPinAfterDeletion(deletedPin: ConsolePin): ConsolePin|null { const pinArray = Array.from(this._pins); for (let i = 0; i < pinArray.length; i++) { if (pinArray[i] === deletedPin) { if (pinArray.length === 1) { return null; } if (i === pinArray.length - 1) { return pinArray[i - 1]; } return pinArray[i + 1]; } } return null; } async doUpdate(): Promise<void> { if (!this._pins.size || !this.isShowing()) { return; } if (this.isShowing()) { this.update(); } const updatePromises = Array.from(this._pins, pin => pin.updatePreview()); await Promise.all(updatePromises); this._updatedForTest(); } _updatedForTest(): void { } } export class ConsolePin extends Common.ObjectWrapper.ObjectWrapper { _pinElement: Element; _pinPreview: HTMLElement; _lastResult: SDK.RuntimeModel.EvaluationResult|null; _lastExecutionContext: SDK.RuntimeModel.ExecutionContext|null; _editor: UI.TextEditor.TextEditor|null; _committedExpression: string; _hovered: boolean; _lastNode: SDK.RemoteObject.RemoteObject|null; _editorPromise: Promise<UI.TextEditor.TextEditor>; constructor(expression: string, pinPane: ConsolePinPane) { super(); const deletePinIcon = (document.createElement('div', {is: 'dt-close-button'}) as UI.UIUtils.DevToolsCloseButton); deletePinIcon.gray = true; deletePinIcon.classList.add('close-button'); deletePinIcon.setTabbable(true); if (expression.length) { deletePinIcon.setAccessibleName(i18nString(UIStrings.removeExpressionS, {PH1: expression})); } else { deletePinIcon.setAccessibleName(i18nString(UIStrings.removeBlankExpression)); } self.onInvokeElement(deletePinIcon, event => { pinPane._removePin(this); event.consume(true); }); const fragment = UI.Fragment.Fragment.build` <div class='console-pin'> ${deletePinIcon} <div class='console-pin-name' $='name'></div> <div class='console-pin-preview' $='preview'></div> </div>`; this._pinElement = fragment.element(); this._pinPreview = (fragment.$('preview') as HTMLElement); const nameElement = (fragment.$('name') as HTMLElement); UI.Tooltip.Tooltip.install(nameElement, expression); elementToConsolePin.set(this._pinElement, this); this._lastResult = null; this._lastExecutionContext = null; this._editor = null; this._committedExpression = expression; this._hovered = false; this._lastNode = null; this._pinPreview.addEventListener('mouseenter', this.setHovered.bind(this, true), false); this._pinPreview.addEventListener('mouseleave', this.setHovered.bind(this, false), false); this._pinPreview.addEventListener('click', (event: Event) => { if (this._lastNode) { Common.Revealer.reveal(this._lastNode); event.consume(); } }, false); const createTextEditor = (factory: UI.TextEditor.TextEditorFactory): UI.TextEditor.TextEditor => { this._editor = factory.createEditor({ devtoolsAccessibleName: i18nString(UIStrings.liveExpressionEditor), lineNumbers: false, lineWrapping: true, mimeType: 'javascript', autoHeight: true, placeholder: i18nString(UIStrings.expression), bracketMatchingSetting: undefined, lineWiseCopyCut: undefined, maxHighlightLength: undefined, padBottom: undefined, inputStyle: undefined, }); this._editor.configureAutocomplete( ObjectUI.JavaScriptAutocomplete.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor)); this._editor.widget().show(nameElement); this._editor.widget().element.classList.add('console-pin-editor'); this._editor.widget().element.tabIndex = -1; this._editor.setText(expression); this._editor.widget().element.addEventListener('keydown', event => { if (!this._editor) { return; } if (event.key === 'Tab' && !this._editor.text()) { event.consume(); return; } if (event.keyCode === UI.KeyboardShortcut.Keys.Esc.code) { this._editor.setText(this._committedExpression); } }, true); this._editor.widget().element.addEventListener('focusout', _event => { if (!this._editor) { return; } const text = this._editor.text(); const trimmedText = text.trim(); if (text.length !== trimmedText.length) { this._editor.setText(trimmedText); } this._committedExpression = trimmedText; pinPane._savePins(); if (this._committedExpression.length) { deletePinIcon.setAccessibleName(i18nString(UIStrings.removeExpressionS, {PH1: this._committedExpression})); } else { deletePinIcon.setAccessibleName(i18nString(UIStrings.removeBlankExpression)); } this._editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(Infinity, Infinity)); }); return this._editor; }; const extension = (Root.Runtime.Runtime.instance().extension(UI.TextEditor.TextEditorFactory) as Root.Runtime.Extension); this._editorPromise = extension.instance().then(obj => createTextEditor((obj as UI.TextEditor.TextEditorFactory))); } setHovered(hovered: boolean): void { if (this._hovered === hovered) { return; } this._hovered = hovered; if (!hovered && this._lastNode) { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } } expression(): string { return this._committedExpression; } element(): Element { return this._pinElement; } async focus(): Promise<void> { const editor = await this._editorPromise; editor.widget().focus(); editor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(Infinity, Infinity)); } appendToContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void { if (this._lastResult && !('error' in this._lastResult) && this._lastResult.object) { contextMenu.appendApplicableItems(this._lastResult.object); // Prevent result from being released manually. It will release along with 'console' group. this._lastResult = null; } } async updatePreview(): Promise<void> { if (!this._editor) { return; } const text = this._editor.textWithCurrentSuggestion().trim(); const isEditing = this._pinElement.hasFocus(); const throwOnSideEffect = isEditing && text !== this._committedExpression; const timeout = throwOnSideEffect ? 250 : undefined; const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); const {preview, result} = await ObjectUI.JavaScriptREPL.JavaScriptREPL.evaluateAndBuildPreview( text, throwOnSideEffect, timeout, !isEditing /* allowErrors */, 'console'); if (this._lastResult && this._lastExecutionContext) { this._lastExecutionContext.runtimeModel.releaseEvaluationResult(this._lastResult); } this._lastResult = result || null; this._lastExecutionContext = executionContext || null; const previewText = preview.deepTextContent(); if (!previewText || previewText !== this._pinPreview.deepTextContent()) { this._pinPreview.removeChildren(); if (result && SDK.RuntimeModel.RuntimeModel.isSideEffectFailure(result)) { const sideEffectLabel = (this._pinPreview.createChild('span', 'object-value-calculate-value-button') as HTMLElement); sideEffectLabel.textContent = '(…)'; UI.Tooltip.Tooltip.install(sideEffectLabel, i18nString(UIStrings.evaluateAllowingSideEffects)); } else if (previewText) { this._pinPreview.appendChild(preview); } else if (!isEditing) { UI.UIUtils.createTextChild(this._pinPreview, i18nString(UIStrings.notAvailable)); } UI.Tooltip.Tooltip.install(this._pinPreview, previewText); } let node: SDK.RemoteObject.RemoteObject|null = null; if (result && !('error' in result) && result.object.type === 'object' && result.object.subtype === 'node') { node = result.object; } if (this._hovered) { if (node) { SDK.OverlayModel.OverlayModel.highlightObjectAsDOMNode(node); } else if (this._lastNode) { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } } this._lastNode = node || null; const isError = result && !('error' in result) && result.exceptionDetails && !SDK.RuntimeModel.RuntimeModel.isSideEffectFailure(result); this._pinElement.classList.toggle('error-level', Boolean(isError)); } }