chrome-devtools-frontend
Version:
Chrome DevTools UI
224 lines (196 loc) • 6.26 kB
text/typescript
// Copyright (c) 2023 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.
import '../../../ui/legacy/legacy.js'; // for x-link
import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as TextEditor from '../../../ui/components/text_editor/text_editor.js';
import * as Lit from '../../lit/lit.js';
import * as VisualLogging from '../../visual_logging/visual_logging.js';
import stylesRaw from './codeBlock.css.js';
// TODO(crbug.com/391381439): Fully migrate off of constructed style sheets.
const styles = new CSSStyleSheet();
styles.replaceSync(stylesRaw.cssContent);
const {html} = Lit;
const UIStrings = {
/**
* @description The header text if not present and language is not set.
*/
code: 'Code',
/**
* @description The title of the button to copy the codeblock from a Markdown view.
*/
copy: 'Copy code',
/**
* @description The title of the button after it was pressed and the text was copied to clipboard.
*/
copied: 'Copied to clipboard',
/**
* @description Disclaimer shown in the code blocks.
*/
disclaimer: 'Use code snippets with caution',
};
const str_ = i18n.i18n.registerUIStrings('ui/components/markdown_view/CodeBlock.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class CodeBlock extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#code = '';
#codeLang = '';
#copyTimeout = 1000;
#timer?: ReturnType<typeof setTimeout>;
#copied = false;
#editorState?: CodeMirror.EditorState;
#languageConf = new CodeMirror.Compartment();
/**
* Whether to display a notice "Use code snippets with caution" in code
* blocks.
*/
#displayNotice = false;
#header?: string;
#showCopyButton = true;
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [styles];
this.#render();
}
set code(value: string) {
this.#code = value;
this.#editorState = CodeMirror.EditorState.create({
doc: this.#code,
extensions: [
TextEditor.Config.baseConfiguration(this.#code),
CodeMirror.EditorState.readOnly.of(true),
CodeMirror.EditorView.lineWrapping,
this.#languageConf.of(CodeMirror.javascript.javascript()),
],
});
this.#render();
}
get code(): string {
return this.#code;
}
set codeLang(value: string) {
this.#codeLang = value;
this.#render();
}
set timeout(value: number) {
this.#copyTimeout = value;
this.#render();
}
set displayNotice(value: boolean) {
this.#displayNotice = value;
this.#render();
}
set header(header: string) {
this.#header = header;
this.#render();
}
set showCopyButton(show: boolean) {
this.#showCopyButton = show;
this.#render();
}
#onCopy(): void {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(this.#code);
this.#copied = true;
this.#render();
clearTimeout(this.#timer);
this.#timer = setTimeout(() => {
this.#copied = false;
this.#render();
}, this.#copyTimeout);
}
#renderNotice(): Lit.TemplateResult {
// clang-format off
return html`<p class="notice">
<x-link class="link" href="https://support.google.com/legal/answer/13505487" jslog=${
VisualLogging.link('code-disclaimer').track({
click: true,
})}>
${i18nString(UIStrings.disclaimer)}
</x-link>
</p>`;
// clang-format on
}
#renderCopyButton(): Lit.LitTemplate {
// clang-format off
return html`
<div class="copy-button-container">
<devtools-button
.data=${
{
variant: Buttons.Button.Variant.ICON,
size: Buttons.Button.Size.SMALL,
jslogContext: 'copy',
iconName: 'copy',
title: i18nString(UIStrings.copy),
} as Buttons.Button.ButtonData
}
=${this.#onCopy}
></devtools-button>
${this.#copied ? html`<span>${i18nString(UIStrings.copied)}</span>` : Lit.nothing}
</div>`;
// clang-format on
}
#renderTextEditor(): Lit.TemplateResult {
if (!this.#editorState) {
throw new Error('Unexpected: trying to render the text editor without editorState');
}
// clang-format off
return html`
<div class="code">
<devtools-text-editor .state=${this.#editorState}></devtools-text-editor>
</div>
`;
// clang-format on
}
#render(): void {
const header = (this.#header ?? this.#codeLang) || i18nString(UIStrings.code);
// clang-format off
Lit.render(
html`<div class='codeblock' jslog=${VisualLogging.section('code')}>
<div class="editor-wrapper">
<div class="heading">
<h4 class="heading-text">${header}</h4>
${this.#showCopyButton ? this.#renderCopyButton() : Lit.nothing}
</div>
${this.#renderTextEditor()}
</div>
${this.#displayNotice ? this.#renderNotice() : Lit.nothing}
</div>`,
this.#shadow,
{
host: this,
},
);
// clang-format on
const editor = this.#shadow?.querySelector('devtools-text-editor')?.editor;
if (!editor) {
return;
}
let language = CodeMirror.html.html({autoCloseTags: false, selfClosingTags: true});
switch (this.#codeLang) {
case 'js':
language = CodeMirror.javascript.javascript();
break;
case 'ts':
language = CodeMirror.javascript.javascript({typescript: true});
break;
case 'jsx':
language = CodeMirror.javascript.javascript({jsx: true});
break;
case 'css':
language = CodeMirror.css.css();
break;
}
editor.dispatch({
effects: this.#languageConf.reconfigure(language),
});
}
}
customElements.define('devtools-code-block', CodeBlock);
declare global {
interface HTMLElementTagNameMap {
'devtools-code-block': CodeBlock;
}
}