// Copyright 2023 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../../ui/kit/kit.js'; import '../../ui/components/menus/menus.js'; import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as SuggestionInput from '../../ui/components/suggestion_input/suggestion_input.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as ElementsComponents from '../elements/components/components.js'; import editorWidgetStyles from './JSONEditor.css.js'; const {html, render, Directives, nothing} = Lit; const {live, classMap, repeat} = Directives; const UIStrings = { /** * @description The title of a button that deletes a parameter. */ deleteParameter: 'Delete parameter', /** * @description The title of a button that adds a parameter. */ addParameter: 'Add a parameter', /** * @description The title of a button that reset the value of a parameters to its default value. */ resetDefaultValue: 'Reset to default value', /** * @description The title of a button to add custom key/value pairs to object parameters with no keys defined */ addCustomProperty: 'Add custom property', /** * @description The title of a button that sends a CDP command. */ sendCommandCtrlEnter: 'Send command - Ctrl+Enter', /** * @description The title of a button that sends a CDP command. */ sendCommandCmdEnter: 'Send command - ⌘+Enter', /** * @description The title of a button that copies a CDP command. */ copyCommand: 'Copy command', /** * @description A label for a select input that allows selecting a CDP target to send the commands to. */ selectTarget: 'Select a target', } as const; const str_ = i18n.i18n.registerUIStrings('panels/protocol_monitor/JSONEditor.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export const enum ParameterType { STRING = 'string', NUMBER = 'number', BOOLEAN = 'boolean', ARRAY = 'array', OBJECT = 'object', } interface BaseParameter { optional: boolean; name: string; typeRef?: string; description: string; isCorrectType?: boolean; isKeyEditable?: boolean; } interface ArrayParameter extends BaseParameter { type: ParameterType.ARRAY; value?: Parameter[]; } interface NumberParameter extends BaseParameter { type: ParameterType.NUMBER; value?: number; } interface StringParameter extends BaseParameter { type: ParameterType.STRING; value?: string; } interface BooleanParameter extends BaseParameter { type: ParameterType.BOOLEAN; value?: boolean; } interface ObjectParameter extends BaseParameter { type: ParameterType.OBJECT; value?: Parameter[]; } export type Parameter = ArrayParameter|NumberParameter|StringParameter|BooleanParameter|ObjectParameter; export interface Command { command: string; parameters: Record; targetId?: string; } interface ViewInput { onKeydown: (event: KeyboardEvent) => void; metadataByCommand: Map; command: string; parameters: Parameter[]; typesByName: Map; onCommandInputBlur: (event: Event) => void; onCommandSend: () => void; onCopyToClipboard: () => void; targets: SDK.Target.Target[]; targetId: string|undefined; onAddParameter: (parameterId: string) => void; onClearParameter: (parameter: Parameter, isParentArray?: boolean) => void; onDeleteParameter: (parameter: Parameter, parentParameter: Parameter) => void; onTargetSelected: (event: Event) => void; computeDropdownValues: (parameter: Parameter) => string[]; onParameterFocus: (event: Event) => void; onParameterKeydown: (event: KeyboardEvent) => void; onParameterKeyBlur: (event: Event) => void; onParameterValueBlur: (event: Event) => void; } export type View = (input: ViewInput, output: object, target: HTMLElement) => void; const splitDescription = (description: string): [string, string] => { // If the description is too long we make the UI a bit better by highlighting the first sentence // which contains the most information. // The number 150 has been chosen arbitrarily if (description.length > 150) { const [firstSentence, restOfDescription] = description.split('.'); // To make the UI nicer, we add a dot at the end of the first sentence. firstSentence + '.'; return [firstSentence, restOfDescription]; } return [description, '']; }; const defaultValueByType = new Map([ ['string', ''], ['number', 0], ['boolean', false], ]); const DUMMY_DATA = 'dummy'; const EMPTY_STRING = ''; export function suggestionFilter(option: string, query: string): boolean { return option.toLowerCase().includes(query.toLowerCase()); } export const enum Events { SUBMIT_EDITOR = 'submiteditor', } export interface EventTypes { [Events.SUBMIT_EDITOR]: Command; } export class JSONEditor extends Common.ObjectWrapper.eventMixin(UI.Widget.VBox) { #metadataByCommand = new Map(); #typesByName = new Map(); #enumsByName = new Map>(); #parameters: Parameter[] = []; #targets: SDK.Target.Target[] = []; #command = ''; #targetId?: string; #hintPopoverHelper?: UI.PopoverHelper.PopoverHelper; #view: View; constructor(element: HTMLElement, view = DEFAULT_VIEW) { super(element, {useShadowDom: true}); this.#view = view; this.registerRequiredCSS(editorWidgetStyles); } get metadataByCommand(): Map { return this.#metadataByCommand; } set metadataByCommand( metadataByCommand: Map) { this.#metadataByCommand = metadataByCommand; this.requestUpdate(); } get typesByName(): Map { return this.#typesByName; } set typesByName(typesByName: Map) { this.#typesByName = typesByName; this.requestUpdate(); } get enumsByName(): Map> { return this.#enumsByName; } set enumsByName(enumsByName: Map>) { this.#enumsByName = enumsByName; this.requestUpdate(); } get parameters(): Parameter[] { return this.#parameters; } set parameters(parameters: Parameter[]) { this.#parameters = parameters; this.requestUpdate(); } get targets(): SDK.Target.Target[] { return this.#targets; } set targets(targets: SDK.Target.Target[]) { this.#targets = targets; this.requestUpdate(); } get command(): string { return this.#command; } set command(command: string) { if (this.#command !== command) { this.#command = command; this.requestUpdate(); } } get targetId(): string|undefined { return this.#targetId; } set targetId(targetId: string|undefined) { if (this.#targetId !== targetId) { this.#targetId = targetId; this.requestUpdate(); } } override wasShown(): void { super.wasShown(); this.#hintPopoverHelper = new UI.PopoverHelper.PopoverHelper( this.contentElement, event => this.#handlePopoverDescriptions(event), 'protocol-monitor.hint'); this.#hintPopoverHelper.setDisableOnClick(true); this.#hintPopoverHelper.setTimeout(300); const targetManager = SDK.TargetManager.TargetManager.instance(); targetManager.addEventListener( SDK.TargetManager.Events.AVAILABLE_TARGETS_CHANGED, this.#handleAvailableTargetsChanged, this); this.#handleAvailableTargetsChanged(); this.requestUpdate(); } override willHide(): void { super.willHide(); this.#hintPopoverHelper?.hidePopover(); this.#hintPopoverHelper?.dispose(); const targetManager = SDK.TargetManager.TargetManager.instance(); targetManager.removeEventListener( SDK.TargetManager.Events.AVAILABLE_TARGETS_CHANGED, this.#handleAvailableTargetsChanged, this); } #handleAvailableTargetsChanged(): void { this.targets = SDK.TargetManager.TargetManager.instance().targets(); if (this.targets.length && this.targetId === undefined) { this.targetId = this.targets[0].id(); } } getParameters(): Record { const formatParameterValue = (parameter: Parameter): unknown => { if (parameter.value === undefined) { return; } switch (parameter.type) { case ParameterType.NUMBER: { return Number(parameter.value); } case ParameterType.BOOLEAN: { return Boolean(parameter.value); } case ParameterType.OBJECT: { const nestedParameters: Record = {}; for (const subParameter of parameter.value) { const formattedValue = formatParameterValue(subParameter); if (formattedValue !== undefined) { nestedParameters[subParameter.name] = formatParameterValue(subParameter); } } if (Object.keys(nestedParameters).length === 0) { return undefined; } return nestedParameters; } case ParameterType.ARRAY: { const nestedArrayParameters = []; for (const subParameter of parameter.value) { nestedArrayParameters.push(formatParameterValue(subParameter)); } return nestedArrayParameters.length === 0 ? [] : nestedArrayParameters; } default: { return parameter.value; } } }; const formattedParameters: Record = {}; for (const parameter of this.parameters) { formattedParameters[parameter.name] = formatParameterValue(parameter); } return formatParameterValue({ type: ParameterType.OBJECT, name: DUMMY_DATA, optional: true, value: this.parameters, description: '', }) as Record; } // Displays a command entered in the input bar inside the editor displayCommand(command: string, parameters: Record, targetId?: string): void { this.targetId = targetId; this.command = command; const schema = this.metadataByCommand.get(this.command); if (!schema?.parameters) { return; } this.populateParametersForCommandWithDefaultValues(); const displayedParameters = this.#convertObjectToParameterSchema( '', parameters, { typeRef: DUMMY_DATA, type: ParameterType.OBJECT, name: '', description: '', optional: true, value: [], }, schema.parameters) .value as Parameter[]; const valueByName = new Map(this.parameters.map(param => [param.name, param])); for (const param of displayedParameters) { const existingParam = valueByName.get(param.name); if (existingParam) { existingParam.value = param.value; } } this.requestUpdate(); } #convertObjectToParameterSchema(key: string, value: unknown, schema?: Parameter, initialSchema?: Parameter[]): Parameter { const type = schema?.type || typeof value; const description = schema?.description ?? ''; const optional = schema?.optional ?? true; switch (type) { case ParameterType.STRING: case ParameterType.BOOLEAN: case ParameterType.NUMBER: return this.#convertPrimitiveParameter(key, value, schema); case ParameterType.OBJECT: return this.#convertObjectParameter(key, value, schema, initialSchema); case ParameterType.ARRAY: return this.#convertArrayParameter(key, value, schema); } return { type, name: key, optional, typeRef: schema?.typeRef, value, description, } as Parameter; } #convertPrimitiveParameter(key: string, value: unknown, schema?: Parameter): Parameter { const type = schema?.type || typeof value; const description = schema?.description ?? ''; const optional = schema?.optional ?? true; return { type, name: key, optional, typeRef: schema?.typeRef, value, description, isCorrectType: schema ? this.#isValueOfCorrectType(schema, String(value)) : true, } as Parameter; } #convertObjectParameter(key: string, value: unknown, schema?: Parameter, initialSchema?: Parameter[]): Parameter { const description = schema?.description ?? ''; if (typeof value !== 'object' || value === null) { throw new Error('The value is not an object'); } const typeRef = schema?.typeRef; if (!typeRef) { throw new Error('Every object parameters should have a type ref'); } const nestedType = typeRef === DUMMY_DATA ? initialSchema : this.typesByName.get(typeRef); if (!nestedType) { throw new Error('No nested type for keys were found'); } const objectValues = []; for (const objectKey of Object.keys(value)) { const objectType = nestedType.find(param => param.name === objectKey); objectValues.push( this.#convertObjectToParameterSchema(objectKey, (value as Record)[objectKey], objectType)); } return { type: ParameterType.OBJECT, name: key, optional: schema.optional, typeRef: schema.typeRef, value: objectValues, description, isCorrectType: true, }; } #convertArrayParameter(key: string, value: unknown, schema?: Parameter): Parameter { const description = schema?.description ?? ''; const optional = schema?.optional ?? true; const typeRef = schema?.typeRef; if (!typeRef) { throw new Error('Every array parameters should have a type ref'); } if (!Array.isArray(value)) { throw new Error('The value is not an array'); } const nestedType = isTypePrimitive(typeRef) ? undefined : { optional: true, type: ParameterType.OBJECT as ParameterType.OBJECT, value: [], typeRef, description: '', name: '', }; const objectValues = []; for (let i = 0; i < value.length; i++) { const temp = this.#convertObjectToParameterSchema(`${i}`, value[i], nestedType); objectValues.push(temp); } return { type: ParameterType.ARRAY, name: key, optional, typeRef: schema?.typeRef, value: objectValues, description, isCorrectType: true, }; } #handlePopoverDescriptions(event: MouseEvent|KeyboardEvent): {box: AnchorBox, show: (popover: UI.GlassPane.GlassPane) => Promise}|null { const hintElement = event.composedPath()[0] as HTMLElement; const elementData = this.#getDescriptionAndTypeForElement(hintElement); if (!elementData?.description) { return null; } const [head, tail] = splitDescription(elementData.description); const type = elementData.type; const replyArgs = elementData.replyArgs; let popupContent = ''; // replyArgs and type cannot get into conflict because replyArgs is attached to a command and type to a parameter if (replyArgs && replyArgs.length > 0) { popupContent = tail + `Returns: ${replyArgs}
`; } else if (type) { popupContent = tail + `
Type: ${type}
`; } else { popupContent = tail; } return { box: hintElement.boxInWindow(), show: async (popover: UI.GlassPane.GlassPane) => { const popupElement = new ElementsComponents.CSSHintDetailsView.CSSHintDetailsView({ getMessage: () => `${head}`, getPossibleFixMessage: () => popupContent, getLearnMoreLink: () => `https://chromedevtools.github.io/devtools-protocol/tot/${this.command.split('.')[0]}/`, }); popover.contentElement.appendChild(popupElement); return true; }, }; } #getDescriptionAndTypeForElement(hintElement: HTMLElement): {description: string, type?: ParameterType, replyArgs?: string[]}|undefined { if (hintElement.matches('.command')) { const metadata = this.metadataByCommand.get(this.command); if (metadata) { return {description: metadata.description, replyArgs: metadata.replyArgs}; } } if (hintElement.matches('.parameter')) { const id = hintElement.dataset.paramid; if (!id) { return; } const pathArray = id.split('.'); const {parameter} = this.#getChildByPath(pathArray); if (!parameter.description) { return; } return {description: parameter.description, type: parameter.type}; } return; } getCommandJson(): string { return this.command !== '' ? JSON.stringify({command: this.command, parameters: this.getParameters()}) : ''; } #copyToClipboard(): void { const commandJson = this.getCommandJson(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(commandJson); } #handleCommandSend(): void { this.dispatchEventToListeners(Events.SUBMIT_EDITOR, { command: this.command, parameters: this.getParameters(), targetId: this.targetId, }); } populateParametersForCommandWithDefaultValues(): void { const commandParameters = this.metadataByCommand.get(this.command)?.parameters; if (!commandParameters) { return; } this.parameters = commandParameters.map((parameter: Parameter) => { return this.#populateParameterDefaults(parameter); }); } #populateParameterDefaults(parameter: Parameter): Parameter { if (parameter.type === ParameterType.OBJECT) { let typeRef = parameter.typeRef; if (!typeRef) { typeRef = DUMMY_DATA; } // Fallback to empty array is extremely rare. // It happens when the keys for an object are not registered like for Tracing.MemoryDumpConfig or headers for instance. const nestedTypes = this.typesByName.get(typeRef) ?? []; const nestedParameters = nestedTypes.map(nestedType => { return this.#populateParameterDefaults(nestedType); }); return { ...parameter, value: parameter.optional ? undefined : nestedParameters, isCorrectType: true, } as Parameter; } if (parameter.type === ParameterType.ARRAY) { return { ...parameter, value: parameter?.optional ? undefined : parameter.value?.map(param => this.#populateParameterDefaults(param)) || [], isCorrectType: true, }; } return { ...parameter, value: parameter.optional ? undefined : defaultValueByType.get(parameter.type), isCorrectType: true, } as Parameter; } #getChildByPath(pathArray: string[]): {parameter: Parameter, parentParameter: Parameter} { let parameters = this.parameters; let parentParameter; for (let i = 0; i < pathArray.length; i++) { const name = pathArray[i]; const parameter = parameters.find(param => param.name === name); if (i === pathArray.length - 1) { return {parameter, parentParameter} as {parameter: Parameter, parentParameter: Parameter}; } if (parameter?.type === ParameterType.ARRAY || parameter?.type === ParameterType.OBJECT) { if (parameter.value) { parameters = parameter.value; } } else { throw new Error('Parameter on the path in not an object or an array'); } parentParameter = parameter; } throw new Error('Not found'); } #isValueOfCorrectType(parameter: Parameter, value: string): boolean { if (parameter.type === ParameterType.NUMBER && isNaN(Number(value))) { return false; } // For boolean or array parameters, this will create an array of the values the user can enter const acceptedValues = this.#computeDropdownValues(parameter); // Check to see if the entered value by the user is indeed part of the values accepted by the enum or boolean parameter if (acceptedValues.length !== 0 && !acceptedValues.includes(value)) { return false; } return true; } #saveParameterValue = (event: Event): void => { if (!(event.target instanceof SuggestionInput.SuggestionInput.SuggestionInput)) { return; } let value: string; if (event instanceof KeyboardEvent) { const editableContent = event.target.renderRoot.querySelector('devtools-editable-content'); if (!editableContent) { return; } value = editableContent.innerText; } else { value = event.target.value; } const paramId = event.target.getAttribute('data-paramid'); if (!paramId) { return; } const pathArray = paramId.split('.'); const object = this.#getChildByPath(pathArray).parameter; if (value === '') { object.value = defaultValueByType.get(object.type); } else { object.value = value; object.isCorrectType = this.#isValueOfCorrectType(object, value); } // Needed to render the delete button for object parameters this.requestUpdate(); }; #saveNestedObjectParameterKey = (event: Event): void => { if (!(event.target instanceof SuggestionInput.SuggestionInput.SuggestionInput)) { return; } const value = event.target.value; const paramId = event.target.getAttribute('data-paramid'); if (!paramId) { return; } const pathArray = paramId.split('.'); const {parameter} = this.#getChildByPath(pathArray); parameter.name = value; // Needed to render the delete button for object parameters this.requestUpdate(); }; #handleParameterInputKeydown = (event: KeyboardEvent): void => { if (!(event.target instanceof SuggestionInput.SuggestionInput.SuggestionInput)) { return; } if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { this.#saveParameterValue(event); } }; #handleFocusParameter(event: Event): void { if (!(event.target instanceof SuggestionInput.SuggestionInput.SuggestionInput)) { return; } const paramId = event.target.getAttribute('data-paramid'); if (!paramId) { return; } const pathArray = paramId.split('.'); const object = this.#getChildByPath(pathArray).parameter; object.isCorrectType = true; this.requestUpdate(); } #handleCommandInputBlur = async(event: Event): Promise => { if (event.target instanceof SuggestionInput.SuggestionInput.SuggestionInput) { this.command = event.target.value; } this.populateParametersForCommandWithDefaultValues(); const target = event.target as HTMLElement; await this.updateComplete; this.#focusNextElement(target); }; /** * When devtools-suggestion-input closes, it blurs itself resulting in * the focus shifting to the overall DevTools window. * * This method focuses on the next focusable element (button or input) * so that the focus remains in the Editor and Ctrl + Shift works. */ #focusNextElement(target: HTMLElement): void { // FIXME: can we do this via view output? const elements = this.contentElement.querySelectorAll('devtools-suggestion-input,.add-button') as NodeListOf; const element = [...elements].findIndex(value => value === target.shadowRoot?.host); if (element >= 0 && element + 1 < elements.length) { elements[element + 1].focus(); } else { (this.contentElement.querySelector('devtools-button[jslogcontext="protocol-monitor.send-command"]') as HTMLElement | undefined) ?.focus(); } } #createNestedParameter(type: Parameter, name: string): Parameter { if (type.type === ParameterType.OBJECT) { let typeRef = type.typeRef; if (!typeRef) { typeRef = DUMMY_DATA; } const nestedTypes = this.typesByName.get(typeRef) ?? []; const nestedValue: Parameter[] = nestedTypes.map(nestedType => this.#createNestedParameter(nestedType, nestedType.name)); return { type: ParameterType.OBJECT, name, optional: type.optional, typeRef, value: nestedValue, isCorrectType: true, description: type.description, }; } return { type: type.type, name, optional: type.optional, isCorrectType: true, typeRef: type.typeRef, value: type.optional ? undefined : defaultValueByType.get(type.type), description: type.description, } as Parameter; } #handleAddParameter(parameterId: string): void { const pathArray = parameterId.split('.'); const {parameter, parentParameter} = this.#getChildByPath(pathArray); if (!parameter) { return; } switch (parameter.type) { case ParameterType.ARRAY: { const typeRef = parameter.typeRef; if (!typeRef) { throw new Error('Every array parameter must have a typeRef'); } const nestedType = this.typesByName.get(typeRef) ?? []; const nestedValue: Parameter[] = nestedType.map(type => this.#createNestedParameter(type, type.name)); let type = isTypePrimitive(typeRef) ? typeRef : ParameterType.OBJECT; // If the typeRef is actually a ref to an enum type, the type of the nested param should be a string if (nestedType.length === 0) { if (this.enumsByName.get(typeRef)) { type = ParameterType.STRING; } } // In case the parameter is an optional array, its value will be undefined so before pushing new value inside, // we reset it to empty array if (!parameter.value) { parameter.value = []; } parameter.value.push({ type, name: String(parameter.value.length), optional: true, typeRef, value: nestedValue.length !== 0 ? nestedValue : '', description: '', isCorrectType: true, } as Parameter); break; } case ParameterType.OBJECT: { let typeRef = parameter.typeRef; if (!typeRef) { typeRef = DUMMY_DATA; } if (!parameter.value) { parameter.value = []; } if (!this.typesByName.get(typeRef)) { parameter.value.push({ type: ParameterType.STRING, name: '', optional: true, value: '', isCorrectType: true, description: '', isKeyEditable: true, }); break; } const nestedTypes = this.typesByName.get(typeRef) ?? []; const nestedValue: Parameter[] = nestedTypes.map(nestedType => this.#createNestedParameter(nestedType, nestedType.name)); const nestedParameters = nestedTypes.map(nestedType => { return this.#populateParameterDefaults(nestedType); }); if (parentParameter) { parameter.value.push({ type: ParameterType.OBJECT, name: '', optional: true, typeRef, value: nestedValue, isCorrectType: true, description: '', }); } else { parameter.value = nestedParameters; } break; } default: // For non-array and non-object parameters, set the value to the default value if available. parameter.value = defaultValueByType.get(parameter.type); break; } this.requestUpdate(); } #handleClearParameter(parameter: Parameter, isParentArray?: boolean): void { if (parameter?.value === undefined) { return; } switch (parameter.type) { case ParameterType.OBJECT: if (parameter.optional && !isParentArray) { parameter.value = undefined; break; } if (!parameter.typeRef || !this.typesByName.get(parameter.typeRef)) { parameter.value = []; } else { parameter.value.forEach(param => this.#handleClearParameter(param, isParentArray)); } break; case ParameterType.ARRAY: parameter.value = parameter.optional ? undefined : []; break; default: parameter.value = parameter.optional ? undefined : defaultValueByType.get(parameter.type); parameter.isCorrectType = true; break; } this.requestUpdate(); } #handleDeleteParameter(parameter: Parameter, parentParameter: Parameter): void { if (!parameter) { return; } if (!Array.isArray(parentParameter.value)) { return; } parentParameter.value.splice(parentParameter.value.findIndex(p => p === parameter), 1); if (parentParameter.type === ParameterType.ARRAY) { for (let i = 0; i < parentParameter.value.length; i++) { parentParameter.value[i].name = String(i); } } this.requestUpdate(); } #onTargetSelected(event: Event): void { if (event.target instanceof HTMLSelectElement) { this.targetId = event.target.value; } this.requestUpdate(); } #computeDropdownValues(parameter: Parameter): string[] { // The suggestion box should only be shown for parameters of type string and boolean if (parameter.type === ParameterType.STRING) { const enums = this.enumsByName.get(`${parameter.typeRef}`) ?? {}; return Object.values(enums); } if (parameter.type === ParameterType.BOOLEAN) { return ['true', 'false']; } return []; } override performUpdate(): void { const viewInput = { onParameterValueBlur: (event: Event): void => { this.#saveParameterValue(event); }, onParameterKeydown: (event: KeyboardEvent): void => { this.#handleParameterInputKeydown(event); }, onParameterFocus: (event: Event): void => { this.#handleFocusParameter(event); }, onParameterKeyBlur: (event: Event): void => { this.#saveNestedObjectParameterKey(event); }, onKeydown: (event: KeyboardEvent): void => { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { this.#handleParameterInputKeydown(event); this.#handleCommandSend(); } }, parameters: this.parameters, metadataByCommand: this.metadataByCommand, command: this.command, typesByName: this.typesByName, onCommandInputBlur: (event: Event) => this.#handleCommandInputBlur(event), onCommandSend: () => this.#handleCommandSend(), onCopyToClipboard: () => this.#copyToClipboard(), targets: this.targets, targetId: this.targetId, onAddParameter: (parameterId: string) => { this.#handleAddParameter(parameterId); }, onClearParameter: (parameter: Parameter, isParentArray?: boolean) => { this.#handleClearParameter(parameter, isParentArray); }, onDeleteParameter: (parameter: Parameter, parentParameter: Parameter) => { this.#handleDeleteParameter(parameter, parentParameter); }, onTargetSelected: (event: Event) => { this.#onTargetSelected(event); }, computeDropdownValues: (parameter: Parameter) => { return this.#computeDropdownValues(parameter); }, }; const viewOutput = {}; this.#view(viewInput, viewOutput, this.contentElement); } } function isTypePrimitive(type: string): boolean { if (type === ParameterType.STRING || type === ParameterType.BOOLEAN || type === ParameterType.NUMBER) { return true; } return false; } function renderTargetSelectorRow(input: ViewInput): Lit.TemplateResult|undefined { // clang-format off return html`
target:
`; // clang-format on } function renderInlineButton(opts: { title: string, iconName: string, classMap: Record, onClick: (event: MouseEvent) => void, jslogContext: string, }): Lit.TemplateResult|undefined { return html` `; } function renderWarningIcon(): Lit.TemplateResult|undefined { return html` `; } /** * Renders the parameters list corresponding to a specific CDP command. */ function renderParameters( input: ViewInput, parameters: Parameter[], id?: string, parentParameter?: Parameter, parentParameterId?: string): Lit.TemplateResult|undefined { parameters.sort((a, b) => Number(a.optional) - Number(b.optional)); // clang-format off return html`
    ${repeat(parameters, parameter => { const parameterId = parentParameter ? `${parentParameterId}` + '.' + `${parameter.name}` : parameter.name; const subparameters: Parameter[] = parameter.type === ParameterType.ARRAY || parameter.type === ParameterType.OBJECT ? (parameter.value ?? []) : []; const isPrimitive = isTypePrimitive(parameter.type); const isArray = parameter.type === ParameterType.ARRAY; const isParentArray = parentParameter && parentParameter.type === ParameterType.ARRAY; const isParentObject = parentParameter && parentParameter.type === ParameterType.OBJECT; const isObject = parameter.type === ParameterType.OBJECT; const isParamValueUndefined = parameter.value === undefined; const isParamOptional = parameter.optional; const hasTypeRef = isObject && parameter.typeRef && input.typesByName.get(parameter.typeRef) !== undefined; // This variable indicates that this parameter is a parameter nested inside an object parameter // that no keys defined inside the CDP documentation. const hasNoKeys = parameter.isKeyEditable; const isCustomEditorDisplayed = isObject && !hasTypeRef; const hasOptions = parameter.type === ParameterType.STRING || parameter.type === ParameterType.BOOLEAN; const canClearParameter = (isArray && !isParamValueUndefined && parameter.value?.length !== 0) || (isObject && !isParamValueUndefined); const parametersClasses = { 'optional-parameter': parameter.optional, parameter: true, 'undefined-parameter': parameter.value === undefined && parameter.optional, }; const inputClasses = { 'json-input': true, }; return html`
  • ${!parameter.isCorrectType ? html`${renderWarningIcon()}` : nothing}
    ${hasNoKeys ? html``} @blur=${input.onParameterKeyBlur} @focus=${input.onParameterFocus} @keydown=${input.onParameterKeydown} >`: html`${parameter.name}`} :
    ${isArray ? html` ${renderInlineButton({ title: i18nString(UIStrings.addParameter), iconName: 'plus', onClick: () => input.onAddParameter(parameterId), classMap: { 'add-button': true }, jslogContext: 'protocol-monitor.add-parameter', })} `: nothing} ${canClearParameter ? renderInlineButton({ title: i18nString(UIStrings.resetDefaultValue), iconName: 'clear', onClick: () => input.onClearParameter(parameter, isParentArray), classMap: {'clear-button': true}, jslogContext: 'protocol-monitor.reset-to-default-value', }) : nothing} ${isPrimitive && !isParentArray && isParamOptional && isParamValueUndefined ? html` ${renderInlineButton({ title: i18nString(UIStrings.addParameter), iconName: 'plus', onClick: () => input.onAddParameter(parameterId), classMap: { 'add-button': true }, jslogContext: 'protocol-monitor.add-parameter', })}` : nothing} ${isObject && isParamOptional && isParamValueUndefined && hasTypeRef ? html` ${renderInlineButton({ title: i18nString(UIStrings.addParameter), iconName: 'plus', onClick: () => input.onAddParameter(parameterId), classMap: { 'add-button': true }, jslogContext: 'protocol-monitor.add-parameter', })}` : nothing}
    ${hasNoKeys && isParentObject ? html` `} .jslogContext=${'parameter-value'} @blur=${input.onParameterValueBlur} @focus=${input.onParameterFocus} @keydown=${input.onParameterKeydown} > ${renderInlineButton({ title: i18nString(UIStrings.deleteParameter), iconName: 'bin', onClick: () => input.onDeleteParameter(parameter, parentParameter), classMap: { deleteButton: true, deleteIcon: true }, jslogContext: 'protocol-monitor.delete-parameter', })}`: nothing} ${isPrimitive && !hasNoKeys && (!isParamValueUndefined || !isParamOptional) && (!isParentArray) ? html` `} .jslogContext=${'parameter-value'} @blur=${input.onParameterValueBlur} @focus=${input.onParameterFocus} @keydown=${input.onParameterKeydown} >` : nothing} ${isPrimitive &&!hasNoKeys && !isParentArray && isParamOptional && !isParamValueUndefined ? html` ${renderInlineButton({ title: i18nString(UIStrings.resetDefaultValue), iconName: 'clear', onClick: () => input.onClearParameter(parameter), classMap: { 'clear-button': true }, jslogContext: 'protocol-monitor.reset-to-default-value', })}` : nothing} ${isCustomEditorDisplayed ? html` ${renderInlineButton({ title: i18nString(UIStrings.addCustomProperty), iconName: 'plus', onClick: () => input.onAddParameter(parameterId), classMap: { 'add-button': true }, jslogContext: 'protocol-monitor.add-custom-property', })} ` : nothing} ${isParentArray ? html` ${!isObject ? html` `} .jslogContext=${'parameter'} @blur=${input.onParameterValueBlur} @keydown=${input.onParameterKeydown} class=${classMap(inputClasses)} >` : nothing} ${renderInlineButton({ title: i18nString(UIStrings.deleteParameter), iconName: 'bin', onClick: () => input.onDeleteParameter(parameter, parentParameter), classMap: { 'delete-button': true }, jslogContext: 'protocol-monitor.delete-parameter', })}` : nothing}
  • ${renderParameters(input, subparameters, id, parameter, parameterId)} `; })}
`; // clang-format on } export const DEFAULT_VIEW: View = (input, _output, target) => { // clang-format off render(html`
${renderTargetSelectorRow(input)}
command:
${input.parameters.length ? html`
parameters:
${renderParameters(input, input.parameters)} ` : nothing}
`, target); // clang-format on };