mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
Change-Id: I98fbb09aca094d5493923dd595c940f9f4bc9e85 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1924206 Commit-Queue: Paul Lewis <aerotwist@chromium.org> Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
310 lines
9.4 KiB
JavaScript
310 lines
9.4 KiB
JavaScript
// 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.
|
|
|
|
const _PinSymbol = Symbol('pinSymbol');
|
|
|
|
export default class ConsolePinPane extends UI.ThrottledWidget {
|
|
/**
|
|
* @param {!UI.ToolbarButton} liveExpressionButton
|
|
*/
|
|
constructor(liveExpressionButton) {
|
|
super(true, 250);
|
|
this._liveExpressionButton = liveExpressionButton;
|
|
this.registerRequiredCSS('console/consolePinPane.css');
|
|
this.registerRequiredCSS('object_ui/objectValue.css');
|
|
this.contentElement.classList.add('console-pins', 'monospace');
|
|
this.contentElement.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false);
|
|
|
|
/** @type {!Set<!Console.ConsolePin>} */
|
|
this._pins = new Set();
|
|
this._pinsSetting = Common.settings.createLocalSetting('consolePins', []);
|
|
for (const expression of this._pinsSetting.get()) {
|
|
this.addPin(expression);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
willHide() {
|
|
for (const pin of this._pins) {
|
|
pin.setHovered(false);
|
|
}
|
|
}
|
|
|
|
_savePins() {
|
|
const toSave = Array.from(this._pins).map(pin => pin.expression());
|
|
this._pinsSetting.set(toSave);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_contextMenuEventFired(event) {
|
|
const contextMenu = new UI.ContextMenu(event);
|
|
const target = event.deepElementFromPoint();
|
|
if (target) {
|
|
const targetPinElement = target.enclosingNodeOrSelfWithClass('console-pin');
|
|
if (targetPinElement) {
|
|
const targetPin = targetPinElement[_PinSymbol];
|
|
contextMenu.editSection().appendItem(ls`Edit expression`, targetPin.focus.bind(targetPin));
|
|
contextMenu.editSection().appendItem(ls`Remove expression`, this._removePin.bind(this, targetPin));
|
|
targetPin.appendToContextMenu(contextMenu);
|
|
}
|
|
}
|
|
contextMenu.editSection().appendItem(ls`Remove all expressions`, this._removeAllPins.bind(this));
|
|
contextMenu.show();
|
|
}
|
|
|
|
_removeAllPins() {
|
|
for (const pin of this._pins) {
|
|
this._removePin(pin);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Console.ConsolePin} pin
|
|
*/
|
|
_removePin(pin) {
|
|
pin.element().remove();
|
|
this._pins.delete(pin);
|
|
this._savePins();
|
|
this._liveExpressionButton.element.focus();
|
|
}
|
|
|
|
/**
|
|
* @param {string} expression
|
|
* @param {boolean=} userGesture
|
|
*/
|
|
addPin(expression, userGesture) {
|
|
const pin = new ConsolePin(expression, this);
|
|
this.contentElement.appendChild(pin.element());
|
|
this._pins.add(pin);
|
|
this._savePins();
|
|
if (userGesture) {
|
|
pin.focus();
|
|
}
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
doUpdate() {
|
|
if (!this._pins.size || !this.isShowing()) {
|
|
return Promise.resolve();
|
|
}
|
|
if (this.isShowing()) {
|
|
this.update();
|
|
}
|
|
const updatePromises = Array.from(this._pins, pin => pin.updatePreview());
|
|
return Promise.all(updatePromises).then(this._updatedForTest.bind(this));
|
|
}
|
|
|
|
_updatedForTest() {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
export class ConsolePin extends Common.Object {
|
|
/**
|
|
* @param {string} expression
|
|
* @param {!Console.ConsolePinPane} pinPane
|
|
*/
|
|
constructor(expression, pinPane) {
|
|
super();
|
|
const deletePinIcon = UI.Icon.create('smallicon-cross', 'console-delete-pin');
|
|
self.onInvokeElement(deletePinIcon, event => {
|
|
pinPane._removePin(this);
|
|
event.consume(true);
|
|
});
|
|
deletePinIcon.tabIndex = 0;
|
|
UI.ARIAUtils.setAccessibleName(deletePinIcon, ls`Remove expression`);
|
|
UI.ARIAUtils.markAsButton(deletePinIcon);
|
|
|
|
const fragment = UI.Fragment.build`
|
|
<div class='console-pin'>
|
|
${deletePinIcon}
|
|
<div class='console-pin-name' $='name'></div>
|
|
<div class='console-pin-preview' $='preview'>${ls`not available`}</div>
|
|
</div>`;
|
|
this._pinElement = fragment.element();
|
|
this._pinPreview = fragment.$('preview');
|
|
const nameElement = fragment.$('name');
|
|
nameElement.title = expression;
|
|
this._pinElement[_PinSymbol] = this;
|
|
|
|
/** @type {?SDK.RuntimeModel.EvaluationResult} */
|
|
this._lastResult = null;
|
|
/** @type {?SDK.ExecutionContext} */
|
|
this._lastExecutionContext = null;
|
|
/** @type {?UI.TextEditor} */
|
|
this._editor = null;
|
|
this._committedExpression = expression;
|
|
this._hovered = false;
|
|
/** @type {?SDK.RemoteObject} */
|
|
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 => {
|
|
if (this._lastNode) {
|
|
Common.Revealer.reveal(this._lastNode);
|
|
event.consume();
|
|
}
|
|
}, false);
|
|
|
|
this._editorPromise = self.runtime.extension(UI.TextEditorFactory).instance().then(factory => {
|
|
this._editor = factory.createEditor({
|
|
devtoolsAccessibleName: ls`Live expression editor`,
|
|
lineNumbers: false,
|
|
lineWrapping: true,
|
|
mimeType: 'javascript',
|
|
autoHeight: true,
|
|
placeholder: ls`Expression`
|
|
});
|
|
this._editor.configureAutocomplete(ObjectUI.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 (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 => {
|
|
const text = this._editor.text();
|
|
const trimmedText = text.trim();
|
|
if (text.length !== trimmedText.length) {
|
|
this._editor.setText(trimmedText);
|
|
}
|
|
this._committedExpression = trimmedText;
|
|
pinPane._savePins();
|
|
this._editor.setSelection(TextUtils.TextRange.createFromLocation(Infinity, Infinity));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} hovered
|
|
*/
|
|
setHovered(hovered) {
|
|
if (this._hovered === hovered) {
|
|
return;
|
|
}
|
|
this._hovered = hovered;
|
|
if (!hovered && this._lastNode) {
|
|
SDK.OverlayModel.hideDOMNodeHighlight();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {string}
|
|
*/
|
|
expression() {
|
|
return this._committedExpression;
|
|
}
|
|
|
|
/**
|
|
* @return {!Element}
|
|
*/
|
|
element() {
|
|
return this._pinElement;
|
|
}
|
|
|
|
async focus() {
|
|
await this._editorPromise;
|
|
this._editor.widget().focus();
|
|
this._editor.setSelection(TextUtils.TextRange.createFromLocation(Infinity, Infinity));
|
|
}
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
*/
|
|
appendToContextMenu(contextMenu) {
|
|
if (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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
*/
|
|
async updatePreview() {
|
|
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.flavor(SDK.ExecutionContext);
|
|
const {preview, result} = await ObjectUI.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.isSideEffectFailure(result)) {
|
|
const sideEffectLabel = this._pinPreview.createChild('span', 'object-value-calculate-value-button');
|
|
sideEffectLabel.textContent = `(...)`;
|
|
sideEffectLabel.title = ls`Evaluate, allowing side effects`;
|
|
} else if (previewText) {
|
|
this._pinPreview.appendChild(preview);
|
|
} else if (!isEditing) {
|
|
this._pinPreview.createTextChild(ls`not available`);
|
|
}
|
|
this._pinPreview.title = previewText;
|
|
}
|
|
|
|
let node = null;
|
|
if (result && result.object && result.object.type === 'object' && result.object.subtype === 'node') {
|
|
node = result.object;
|
|
}
|
|
if (this._hovered) {
|
|
if (node) {
|
|
SDK.OverlayModel.highlightObjectAsDOMNode(node);
|
|
} else if (this._lastNode) {
|
|
SDK.OverlayModel.hideDOMNodeHighlight();
|
|
}
|
|
}
|
|
this._lastNode = node || null;
|
|
|
|
const isError = result && result.exceptionDetails && !SDK.RuntimeModel.isSideEffectFailure(result);
|
|
this._pinElement.classList.toggle('error-level', !!isError);
|
|
}
|
|
}
|
|
|
|
/* Legacy exported object */
|
|
self.Console = self.Console || {};
|
|
|
|
/* Legacy exported object */
|
|
Console = Console || {};
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
Console.ConsolePinPane = ConsolePinPane;
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
Console.ConsolePin = ConsolePin;
|
|
|
|
Console.ConsolePin._PinSymbol = _PinSymbol;
|