UNPKG

chrome-devtools-frontend

Version:
357 lines (326 loc) • 12.7 kB
// Copyright 2025 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. /* * Copyright (C) 2008 Nokia Inc. All rights reserved. * Copyright (C) 2013 Samsung Electronics. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* eslint no-return-assign: "off" */ import * as i18n from '../../core/i18n/i18n.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Directives as LitDirectives, html, nothing, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {StorageItemsView} from './StorageItemsView.js'; const {ARIAUtils} = UI; const {EmptyWidget} = UI.EmptyWidget; const {SplitWidget} = UI.SplitWidget; const {Widget, widgetRef, VBox, widgetConfig} = UI.Widget; const {Size} = UI.Geometry; const {ref, repeat} = LitDirectives; type Widget = UI.Widget.Widget; type SplitWidget = UI.SplitWidget.SplitWidget; type VBox = UI.Widget.VBox; const UIStrings = { /** *@description Text that shows in the Applicaiton Panel if no value is selected for preview */ noPreviewSelected: 'No value selected', /** *@description Preview text when viewing storage in Application panel */ selectAValueToPreview: 'Select a value to preview', /** *@description Text for announcing number of entries after filtering *@example {5} PH1 */ numberEntries: 'Number of entries shown in table: {PH1}', /** *@description Text in DOMStorage Items View of the Application panel */ key: 'Key', /** *@description Text for the value of something */ value: 'Value', }; const str_ = i18n.i18n.registerUIStrings('panels/application/KeyValueStorageItemsView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ViewInput { items: {key: string, value: string}[]; selectedKey: string|null; editable: boolean; onSelect: (event: CustomEvent<HTMLElement|null>) => void; onSort: (event: CustomEvent<{columnId: string, ascending: boolean}>) => void; onCreate: (event: CustomEvent<{key: string, value: string}>) => void; onReferesh: () => void; onEdit: (event: CustomEvent<{node: HTMLElement, columnId: string, valueBeforeEditing: string, newText: string}>) => void; onDelete: (event: CustomEvent<HTMLElement>) => void; } export interface ViewOutput { splitWidget: UI.SplitWidget.SplitWidget; preview: VBox; resizer?: Element; } export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; /** * A helper typically used in the Application panel. Renders a split view * between a DataGrid displaying key-value pairs and a preview Widget. */ export abstract class KeyValueStorageItemsView extends StorageItemsView { #splitWidget!: SplitWidget; #previewPanel!: VBox; #preview: Widget|null; #previewValue: string|null; #items: {key: string, value: string}[] = []; #selectedKey: string|null = null; #view: View; #resizer!: HTMLElement; #isSortOrderAscending = true; #editable: boolean; constructor(title: string, id: string, editable: boolean, view?: View) { if (!view) { view = (input: ViewInput, output: ViewOutput, target: HTMLElement) => { // clang-format off render(html ` <devtools-split-widget .options=${{vertical: false, secondaryIsSidebar: true, settingName: `${id}-split-view-state`}} ${widgetRef(SplitWidget, e => {output.splitWidget = e;})}> <devtools-widget slot="main" .widgetConfig=${widgetConfig(VBox, {minimumSize: new Size(0, 50)})}> <devtools-data-grid .name=${`${id}-datagrid-with-preview`} striped style="flex: auto" @select=${input.onSelect} @sort=${input.onSort} @refresh=${input.onReferesh} @create=${input.onCreate} @edit=${input.onEdit} @delete=${input.onDelete} > <table> <tr> <th id="key" sortable ?editable=${input.editable}> ${i18nString(UIStrings.key)} </th> <th id="value" ?editable=${input.editable}> ${i18nString(UIStrings.value)} </th> </tr> ${repeat(input.items, item => item.key, item => html` <tr data-key=${item.key} data-value=${item.value} selected=${(input.selectedKey === item.key) || nothing}> <td>${item.key}</td> <td>${item.value}</td> </tr>`)} <tr placeholder></tr> </table> </devtools-data-grid> </devtools-widget> <devtools-widget slot="sidebar" .widgetConfig=${widgetConfig(VBox, {minimumSize: new Size(0, 50)})} jslog=${VisualLogging.pane('preview').track({resize: true})} ${widgetRef(VBox, e => {output.preview = e;})}> <div class="preview-panel-resizer" ${ref(e => {output.resizer = e;})}></div> </devtools-widget> </devtools-split-widget>`, // clang-format on target, {host: input}); }; } super(title, id); this.#editable = editable; this.#view = view; this.performUpdate(); this.#splitWidget.installResizer(this.#resizer); this.#splitWidget.setSecondIsSidebar(true); this.#preview = null; this.#previewValue = null; this.showPreview(null, null); } override performUpdate(): void { const viewInput = { items: this.#items, selectedKey: this.#selectedKey, editable: this.#editable, onSelect: (event: CustomEvent<HTMLElement|null>) => { this.setCanDeleteSelected(Boolean(event.detail)); if (!event.detail) { void this.#previewEntry(null); } else { void this.#previewEntry({key: event.detail.dataset.key || '', value: event.detail.dataset.value || ''}); } }, onSort: (event: CustomEvent<{columnId: string, ascending: boolean}>) => { this.#isSortOrderAscending = event.detail.ascending; }, onCreate: (event: CustomEvent<{key: string, value: string}>) => { this.#createCallback(event.detail.key, event.detail.value); }, onEdit: (event: CustomEvent<{node: HTMLElement, columnId: string, valueBeforeEditing: string, newText: string}>) => { this.#editingCallback( event.detail.node, event.detail.columnId, event.detail.valueBeforeEditing, event.detail.newText); }, onDelete: (event: CustomEvent<HTMLElement>) => { this.#deleteCallback(event.detail.dataset.key || ''); }, onReferesh: () => { this.refreshItems(); }, }; const viewOutput = Object.defineProperties({} as ViewOutput, { splitWidget: {set: (v: SplitWidget) => this.#splitWidget = v}, preview: {set: (v: VBox) => this.#previewPanel = v}, resizer: {set: (v: HTMLElement) => this.#resizer = v}, }); this.#view(viewInput, viewOutput, this.contentElement); } get previewPanelForTesting(): VBox { return this.#previewPanel; } itemsCleared(): void { this.#items = []; this.performUpdate(); this.setCanDeleteSelected(false); } itemRemoved(key: string): void { const index = this.#items.findIndex(item => item.key === key); if (index === -1) { return; } this.#items.splice(index, 1); this.performUpdate(); this.setCanDeleteSelected(this.#items.length > 1); } itemAdded(key: string, value: string): void { if (this.#items.some(item => item.key === key)) { return; } this.#items.push({key, value}); this.performUpdate(); } itemUpdated(key: string, value: string): void { const item = this.#items.find(item => item.key === key); if (!item) { return; } if (item.value === value) { return; } item.value = value; this.performUpdate(); if (this.#selectedKey !== key) { return; } if (this.#previewValue !== value) { void this.#previewEntry({key, value}); } this.setCanDeleteSelected(true); } showItems(items: {key: string, value: string}[]): void { const sortDirection = this.#isSortOrderAscending ? 1 : -1; this.#items = [...items].sort((item1, item2) => sortDirection * (item1.key > item2.key ? 1 : -1)); const selectedItem = this.#items.find(item => item.key === this.#selectedKey); if (!selectedItem) { this.#selectedKey = null; } else { void this.#previewEntry(selectedItem); } this.performUpdate(); this.setCanDeleteSelected(Boolean(this.#selectedKey)); ARIAUtils.alert(i18nString(UIStrings.numberEntries, {PH1: this.#items.length})); } override deleteSelectedItem(): void { if (!this.#selectedKey) { return; } this.#deleteCallback(this.#selectedKey); } #createCallback(key: string, value: string): void { this.setItem(key, value); this.#removeDupes(key, value); } #editingCallback(editingNode: HTMLElement, columnIdentifier: string, oldText: string, newText: string): void { if (columnIdentifier === 'key') { if (typeof oldText === 'string') { this.removeItem(oldText); } this.setItem(newText, editingNode.dataset.value || ''); this.#removeDupes(newText, editingNode.dataset.value || ''); editingNode.dataset.key = newText; } else { this.setItem(editingNode.dataset.key || '', newText); } } #removeDupes(key: string, value: string): void { for (let i = this.#items.length - 1; i >= 0; --i) { const child = this.#items[i]; if ((child.key === key) && (value !== child.value)) { this.#items.splice(i, 1); } } } #deleteCallback(key: string): void { this.removeItem(key); } showPreview(preview: Widget|null, value: string|null): void { if (this.#preview && this.#previewValue === value) { return; } if (this.#preview) { this.#preview.detach(); } if (!preview) { preview = new EmptyWidget(i18nString(UIStrings.noPreviewSelected), i18nString(UIStrings.selectAValueToPreview)); } this.#previewValue = value; this.#preview = preview; preview.show(this.#previewPanel.contentElement); } async #previewEntry(entry: {key: string, value: string}|null): Promise<void> { const value = entry && entry.value; if (value) { this.#selectedKey = entry.key; const preview = await this.createPreview(entry.key, value as string); // Selection could've changed while the preview was loaded if (this.#selectedKey === entry.key) { this.showPreview(preview, value); } } else { this.#selectedKey = null; this.showPreview(null, value); } } set editable(editable: boolean) { this.#editable = editable; this.performUpdate(); } protected abstract setItem(key: string, value: string): void; protected abstract removeItem(key: string): void; protected abstract createPreview(key: string, value: string): Promise<Widget|null>; }