UNPKG

chrome-devtools-frontend

Version:
440 lines (390 loc) • 12.5 kB
// Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-lit-render-outside-of-view */ import '../../../ui/kit/kit.js'; import '../../../ui/legacy/legacy.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Input from '../../../ui/components/input/input.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import {findFlexContainerIcon, findGridContainerIcon, type IconInfo} from './CSSPropertyIconResolver.js'; import stylePropertyEditorStyles from './stylePropertyEditor.css.js'; const UIStrings = { /** * @description Title of the button that selects a flex property. * @example {flex-direction} propertyName * @example {column} propertyValue */ selectButton: 'Add {propertyName}: {propertyValue}', /** * @description Title of the button that deselects a flex property. * @example {flex-direction} propertyName * @example {row} propertyValue */ deselectButton: 'Remove {propertyName}: {propertyValue}', /** * @description Label for the dense checkbox in the grid-auto-flow editor. */ denseLabel: 'Dense', } as const; const str_ = i18n.i18n.registerUIStrings('panels/elements/components/StylePropertyEditor.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const {render, html, Directives} = Lit; declare global { interface HTMLElementEventMap { propertyselected: PropertySelectedEvent; propertydeselected: PropertyDeselectedEvent; } } interface FlexEditorData { authoredProperties: Map<string, string>; computedProperties: Map<string, string>; } interface EditableProperty { propertyName: string; propertyValues: string[]; } export class PropertySelectedEvent extends Event { static readonly eventName = 'propertyselected'; data: {name: string, value: string}; constructor(name: string, value: string) { super(PropertySelectedEvent.eventName, {}); this.data = {name, value}; } } export class PropertyDeselectedEvent extends Event { static readonly eventName = 'propertydeselected'; data: {name: string, value: string}; constructor(name: string, value: string) { super(PropertyDeselectedEvent.eventName, {}); this.data = {name, value}; } } export class StylePropertyEditor extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); #authoredProperties = new Map<string, string>(); #computedProperties = new Map<string, string>(); protected readonly editableProperties: EditableProperty[] = []; getEditableProperties(): EditableProperty[] { return this.editableProperties; } set data(data: FlexEditorData) { this.#authoredProperties = data.authoredProperties; this.#computedProperties = data.computedProperties; this.#render(); } #render(): void { // Disabled until https://crbug.com/1079231 is fixed. // clang-format off render(html` <style>${stylePropertyEditorStyles}</style> <style>${Input.checkboxStyles}</style> <div class="container"> ${this.editableProperties.map(prop => this.#renderProperty(prop))} </div> `, this.#shadow, { host: this, }); // clang-format on } #renderProperty(prop: EditableProperty): Lit.TemplateResult { const authoredValue = this.#authoredProperties.get(prop.propertyName); const notAuthored = !authoredValue; const shownValue = authoredValue || this.#computedProperties.get(prop.propertyName); const classes = Directives.classMap({ 'property-value': true, 'not-authored': notAuthored, }); // Special handling for grid-auto-flow with dense checkbox if (prop.propertyName === 'grid-auto-flow') { return this.#renderGridAutoFlowProperty(prop, shownValue, classes); } return html`<div class="row"> <div class="property"> <span class="property-name">${prop.propertyName}</span>: <span class=${classes}>${shownValue}</span> </div> <div class="buttons"> ${prop.propertyValues.map(value => this.#renderButton(value, prop.propertyName, value === authoredValue))} </div> </div>`; } #renderGridAutoFlowProperty( prop: EditableProperty, shownValue: string|undefined, classes: ReturnType<typeof Directives.classMap>): Lit.TemplateResult { const authoredValue = this.#authoredProperties.get(prop.propertyName); const isDense = authoredValue === 'dense' || authoredValue === 'row dense' || authoredValue === 'column dense'; const isRow = authoredValue === 'row' || authoredValue === 'row dense'; const isColumn = authoredValue === 'column' || authoredValue === 'column dense'; return html`<div class="row"> <div class="property"> <span class="property-name">${prop.propertyName}</span>: <span class=${classes}>${shownValue}</span> </div> <div class="buttons"> ${this.#renderButton('row', prop.propertyName, isRow)} ${this.#renderButton('column', prop.propertyName, isColumn)} <devtools-checkbox .checked=${isDense} @change=${(e: Event) => this.#onDenseCheckboxChange(e, isRow, isColumn)} > ${i18nString(UIStrings.denseLabel)} </devtools-checkbox> </div> </div>`; } #onDenseCheckboxChange(e: Event, isRow: boolean, isColumn: boolean): void { const checked = (e.target as HTMLInputElement).checked; const propertyName = 'grid-auto-flow'; const currentValue = this.#authoredProperties.get(propertyName); let newValue = ''; if (isRow) { newValue = checked ? 'row dense' : 'row'; } else if (isColumn) { newValue = checked ? 'column dense' : 'column'; } else { newValue = checked ? 'dense' : ''; } if (currentValue) { this.dispatchEvent(new PropertyDeselectedEvent(propertyName, currentValue)); } if (newValue) { this.dispatchEvent(new PropertySelectedEvent(propertyName, newValue)); } } #renderButton(propertyValue: string, propertyName: string, selected = false): Lit.TemplateResult { const query = `${propertyName}: ${propertyValue}`; const iconInfo = this.findIcon(query, this.#computedProperties); if (!iconInfo) { throw new Error(`Icon for ${query} is not found`); } const transform = `transform: rotate(${iconInfo.rotate}deg) scale(${iconInfo.scaleX}, ${iconInfo.scaleY})`; const classes = Directives.classMap({ button: true, selected, }); const values = {propertyName, propertyValue}; const title = selected ? i18nString(UIStrings.deselectButton, values) : i18nString(UIStrings.selectButton, values); return html` <button title=${title} class=${classes} jslog=${VisualLogging.item().track({click: true}).context(`${propertyName}-${propertyValue}`)} @click=${() => this.#onButtonClick(propertyName, propertyValue, selected)}> <devtools-icon style=${transform} name=${iconInfo.iconName}> </devtools-icon> </button> `; } #onButtonClick(propertyName: string, propertyValue: string, selected: boolean): void { if (propertyName === 'grid-auto-flow') { const currentValue = this.#authoredProperties.get(propertyName); const isDense = currentValue?.includes('dense') || false; if (selected) { const newValue = isDense ? 'dense' : ''; if (currentValue) { this.dispatchEvent(new PropertyDeselectedEvent(propertyName, currentValue)); } if (newValue) { this.dispatchEvent(new PropertySelectedEvent(propertyName, newValue)); } } else { const newValue = isDense ? `${propertyValue} dense` : propertyValue; if (currentValue) { this.dispatchEvent(new PropertyDeselectedEvent(propertyName, currentValue)); } this.dispatchEvent(new PropertySelectedEvent(propertyName, newValue)); } } else if (selected) { this.dispatchEvent(new PropertyDeselectedEvent(propertyName, propertyValue)); } else { this.dispatchEvent(new PropertySelectedEvent(propertyName, propertyValue)); } } protected findIcon(_query: string, _computedProperties: Map<string, string>): IconInfo|null { throw new Error('Not implemented'); } } export class FlexboxEditor extends StylePropertyEditor { readonly jslogContext = 'cssFlexboxEditor'; protected override readonly editableProperties: EditableProperty[] = FlexboxEditableProperties; protected override findIcon(query: string, computedProperties: Map<string, string>): IconInfo|null { return findFlexContainerIcon(query, computedProperties); } } customElements.define('devtools-flexbox-editor', FlexboxEditor); declare global { interface HTMLElementTagNameMap { 'devtools-flexbox-editor': FlexboxEditor; } } export class GridEditor extends StylePropertyEditor { readonly jslogContext = 'cssGridEditor'; protected override readonly editableProperties: EditableProperty[] = GridEditableProperties; protected override findIcon(query: string, computedProperties: Map<string, string>): IconInfo|null { return findGridContainerIcon(query, computedProperties); } } customElements.define('devtools-grid-editor', GridEditor); declare global { interface HTMLElementTagNameMap { 'devtools-grid-editor': GridEditor; } } export class GridLanesEditor extends StylePropertyEditor { readonly jslogContext = 'cssGridLanesEditor'; protected override readonly editableProperties: EditableProperty[] = GridLanesEditableProperties; protected override findIcon(query: string, computedProperties: Map<string, string>): IconInfo|null { return findGridContainerIcon(query, computedProperties); } } customElements.define('devtools-grid-lanes-editor', GridLanesEditor); declare global { interface HTMLElementTagNameMap { 'devtools-grid-lanes-editor': GridLanesEditor; } } export const FlexboxEditableProperties = [ { propertyName: 'flex-direction', propertyValues: [ 'row', 'column', 'row-reverse', 'column-reverse', ], }, { propertyName: 'flex-wrap', propertyValues: [ 'nowrap', 'wrap', ], }, { propertyName: 'align-content', propertyValues: [ 'center', 'flex-start', 'flex-end', 'space-around', 'space-between', 'stretch', ], }, { propertyName: 'justify-content', propertyValues: [ 'center', 'flex-start', 'flex-end', 'space-between', 'space-around', 'space-evenly', ], }, { propertyName: 'align-items', propertyValues: [ 'center', 'flex-start', 'flex-end', 'stretch', 'baseline', ], }, ]; export const GridEditableProperties = [ { propertyName: 'grid-auto-flow', propertyValues: [ 'row', 'column', ], }, { propertyName: 'align-content', propertyValues: [ 'center', 'start', 'end', 'space-between', 'space-around', 'space-evenly', 'stretch', ], }, { propertyName: 'justify-content', propertyValues: [ 'center', 'start', 'end', 'space-between', 'space-around', 'space-evenly', 'stretch', ], }, { propertyName: 'align-items', propertyValues: [ 'center', 'start', 'end', 'stretch', 'baseline', ], }, { propertyName: 'justify-items', propertyValues: [ 'center', 'start', 'end', 'stretch', ], }, ]; export const GridLanesEditableProperties = [ { propertyName: 'align-content', propertyValues: [ 'center', 'start', 'end', 'space-between', 'space-around', 'space-evenly', 'stretch', ], }, { propertyName: 'justify-content', propertyValues: [ 'center', 'start', 'end', 'space-between', 'space-around', 'space-evenly', 'stretch', ], }, { propertyName: 'align-items', propertyValues: [ 'center', 'start', 'end', 'stretch', ], }, { propertyName: 'justify-items', propertyValues: [ 'center', 'start', 'end', 'stretch', ], }, ];