Alex Rudenko | 128c7b3 | 2023-05-03 11:03:43 | [diff] [blame^] | 1 | // Copyright 2023 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import * as i18n from '../../../core/i18n/i18n.js'; |
| 6 | import * as Buttons from '../../../ui/components/buttons/buttons.js'; |
| 7 | import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js'; |
| 8 | import * as IconButton from '../../../ui/components/icon_button/icon_button.js'; |
| 9 | import * as LitHtml from '../../../ui/lit-html/lit-html.js'; |
| 10 | import * as Models from '../models/models.js'; |
| 11 | import * as Actions from '../recorder-actions.js'; // eslint-disable-line rulesdir/es_modules_import |
| 12 | |
| 13 | import recordingListViewStyles from './recordingListView.css.js'; |
| 14 | |
| 15 | const UIStrings = { |
| 16 | /** |
| 17 | *@description The title of the page that contains a list of saved recordings that the user has.. |
| 18 | */ |
| 19 | savedRecordings: 'Saved recordings', |
| 20 | /** |
| 21 | * @description The title of the button that leads to create a new recording page. |
| 22 | */ |
| 23 | createRecording: 'Create a new recording', |
| 24 | /** |
| 25 | * @description The title of the button that is shown next to each of the recordings and that triggers playing of the recording. |
| 26 | */ |
| 27 | playRecording: 'Play recording', |
| 28 | /** |
| 29 | * @description The title of the button that is shown next to each of the recordings and that triggers deletion of the recording. |
| 30 | */ |
| 31 | deleteRecording: 'Delete recording', |
| 32 | /** |
| 33 | * @description The title of the row corresponding to a recording. By clicking on the row, the user open the recording for editing. |
| 34 | */ |
| 35 | openRecording: 'Open recording', |
| 36 | }; |
| 37 | const str_ = i18n.i18n.registerUIStrings( |
| 38 | 'panels/recorder/components/RecordingListView.ts', |
| 39 | UIStrings, |
| 40 | ); |
| 41 | const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| 42 | |
| 43 | declare global { |
| 44 | interface HTMLElementTagNameMap { |
| 45 | 'devtools-recording-list-view': RecordingListView; |
| 46 | } |
| 47 | |
| 48 | interface HTMLElementEventMap { |
| 49 | openrecording: OpenRecordingEvent; |
| 50 | deleterecording: DeleteRecordingEvent; |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | export class CreateRecordingEvent extends Event { |
| 55 | static readonly eventName = 'createrecording'; |
| 56 | constructor() { |
| 57 | super(CreateRecordingEvent.eventName); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | export class DeleteRecordingEvent extends Event { |
| 62 | static readonly eventName = 'deleterecording'; |
| 63 | constructor(public storageName: string) { |
| 64 | super(DeleteRecordingEvent.eventName); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | export class OpenRecordingEvent extends Event { |
| 69 | static readonly eventName = 'openrecording'; |
| 70 | constructor(public storageName: string) { |
| 71 | super(OpenRecordingEvent.eventName); |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | export class PlayRecordingEvent extends Event { |
| 76 | static readonly eventName = 'playrecording'; |
| 77 | constructor(public storageName: string) { |
| 78 | super(PlayRecordingEvent.eventName); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | interface Recording { |
| 83 | storageName: string; |
| 84 | name: string; |
| 85 | } |
| 86 | |
| 87 | const pathIconUrl = new URL( |
| 88 | '../images/path_icon.svg', |
| 89 | import.meta.url, |
| 90 | ) |
| 91 | .toString(); |
| 92 | const playIconUrl = new URL( |
| 93 | '../images/play_icon.svg', |
| 94 | import.meta.url, |
| 95 | ) |
| 96 | .toString(); |
| 97 | const deleteIconUrl = new URL( |
| 98 | '../images/delete_icon.svg', |
| 99 | import.meta.url, |
| 100 | ) |
| 101 | .toString(); |
| 102 | export class RecordingListView extends HTMLElement { |
| 103 | static readonly litTagName = LitHtml.literal`devtools-recording-list-view`; |
| 104 | readonly #shadow = this.attachShadow({mode: 'open'}); |
| 105 | readonly #props: {recordings: Recording[], replayAllowed: boolean} = { |
| 106 | recordings: [], |
| 107 | replayAllowed: true, |
| 108 | }; |
| 109 | |
| 110 | connectedCallback(): void { |
| 111 | this.#shadow.adoptedStyleSheets = [recordingListViewStyles]; |
| 112 | void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); |
| 113 | } |
| 114 | |
| 115 | set recordings(recordings: Recording[]) { |
| 116 | this.#props.recordings = recordings; |
| 117 | void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); |
| 118 | } |
| 119 | |
| 120 | set replayAllowed(value: boolean) { |
| 121 | this.#props.replayAllowed = value; |
| 122 | void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); |
| 123 | } |
| 124 | |
| 125 | #onCreateClick(): void { |
| 126 | this.dispatchEvent(new CreateRecordingEvent()); |
| 127 | } |
| 128 | |
| 129 | #onDeleteClick(storageName: string, event: Event): void { |
| 130 | event.stopPropagation(); |
| 131 | this.dispatchEvent(new DeleteRecordingEvent(storageName)); |
| 132 | } |
| 133 | |
| 134 | #onOpenClick(storageName: string, event: Event): void { |
| 135 | event.stopPropagation(); |
| 136 | this.dispatchEvent(new OpenRecordingEvent(storageName)); |
| 137 | } |
| 138 | |
| 139 | #onPlayRecordingClick(storageName: string, event: Event): void { |
| 140 | event.stopPropagation(); |
| 141 | this.dispatchEvent(new PlayRecordingEvent(storageName)); |
| 142 | } |
| 143 | |
| 144 | #onKeyDown(storageName: string, event: Event): void { |
| 145 | if ((event as KeyboardEvent).key !== 'Enter') { |
| 146 | return; |
| 147 | } |
| 148 | this.#onOpenClick(storageName, event); |
| 149 | } |
| 150 | |
| 151 | #stopPropagation(event: Event): void { |
| 152 | event.stopPropagation(); |
| 153 | } |
| 154 | |
| 155 | #render = (): void => { |
| 156 | // clang-format off |
| 157 | LitHtml.render( |
| 158 | LitHtml.html` |
| 159 | <div class="wrapper"> |
| 160 | <div class="header"> |
| 161 | <h1>${i18nString(UIStrings.savedRecordings)}</h1> |
| 162 | <${Buttons.Button.Button.litTagName} |
| 163 | .variant=${Buttons.Button.Variant.PRIMARY} |
| 164 | @click=${this.#onCreateClick} |
| 165 | title=${Models.Tooltip.getTooltipForActions( |
| 166 | i18nString(UIStrings.createRecording), |
| 167 | Actions.RecorderActions.CreateRecording, |
| 168 | )} |
| 169 | > |
| 170 | ${i18nString(UIStrings.createRecording)} |
| 171 | </${Buttons.Button.Button.litTagName}> |
| 172 | </div> |
| 173 | <div class="table"> |
| 174 | ${this.#props.recordings.map(recording => { |
| 175 | return LitHtml.html` |
| 176 | <div role="button" tabindex="0" aria-label=${i18nString( |
| 177 | UIStrings.openRecording, |
| 178 | )} class="row" @keydown=${this.#onKeyDown.bind( |
| 179 | this, |
| 180 | recording.storageName, |
| 181 | )} @click=${this.#onOpenClick.bind(this, recording.storageName)}> |
| 182 | <div class="icon"> |
| 183 | <${IconButton.Icon.Icon.litTagName} .data=${ |
| 184 | { |
| 185 | iconPath: pathIconUrl, |
| 186 | color: 'var(--color-primary-old)', |
| 187 | } as IconButton.Icon.IconData |
| 188 | }> |
| 189 | </${IconButton.Icon.Icon.litTagName}> |
| 190 | </div> |
| 191 | <div class="title">${recording.name}</div> |
| 192 | <div class="actions"> |
| 193 | ${ |
| 194 | this.#props.replayAllowed |
| 195 | ? LitHtml.html`<button title=${i18nString( |
| 196 | UIStrings.playRecording, |
| 197 | )} @keydown=${ |
| 198 | this.#stopPropagation |
| 199 | } @click=${this.#onPlayRecordingClick.bind( |
| 200 | this, |
| 201 | recording.storageName, |
| 202 | )}> |
| 203 | <${IconButton.Icon.Icon.litTagName} .data=${ |
| 204 | { |
| 205 | iconPath: playIconUrl, |
| 206 | color: 'var(--color-text-primary)', |
| 207 | } as IconButton.Icon.IconData |
| 208 | }> |
| 209 | </${IconButton.Icon.Icon.litTagName}> |
| 210 | </button><div class="divider"></div>` |
| 211 | : '' |
| 212 | } |
| 213 | <button class="delete-recording-button" title=${i18nString( |
| 214 | UIStrings.deleteRecording, |
| 215 | )} @keydown=${ |
| 216 | this.#stopPropagation |
| 217 | } @click=${this.#onDeleteClick.bind(this, recording.storageName)}> |
| 218 | <${IconButton.Icon.Icon.litTagName} .data=${ |
| 219 | { |
| 220 | iconPath: deleteIconUrl, |
| 221 | color: 'var(--color-text-primary)', |
| 222 | } as IconButton.Icon.IconData |
| 223 | }> |
| 224 | </${IconButton.Icon.Icon.litTagName}> |
| 225 | </button> |
| 226 | </div> |
| 227 | </div> |
| 228 | `; |
| 229 | })} |
| 230 | </div> |
| 231 | </div> |
| 232 | `, |
| 233 | this.#shadow, |
| 234 | { host: this }, |
| 235 | ); |
| 236 | // clang-format on |
| 237 | }; |
| 238 | } |
| 239 | |
| 240 | ComponentHelpers.CustomElements.defineComponent( |
| 241 | 'devtools-recording-list-view', |
| 242 | RecordingListView, |
| 243 | ); |