blob: 99d6f19685dfcba0240d05bbc4bff011843960f3 [file] [log] [blame]
// Copyright (c) 2021 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 * as i18n from '../../../core/i18n/i18n.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import stylePropertyEditorStyles from './stylePropertyEditor.css.js';
import {findFlexContainerIcon, findGridContainerIcon, type IconInfo} from './CSSPropertyIconResolver.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}',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/components/StylePropertyEditor.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {render, html, Directives} = LitHtml;
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};
}
}
// eslint-disable-next-line rulesdir/check_component_naming
export class StylePropertyEditor extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#authoredProperties: Map<string, string> = new Map();
#computedProperties: Map<string, string> = new Map();
protected readonly editableProperties: EditableProperty[] = [];
constructor() {
super();
}
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [stylePropertyEditorStyles];
}
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`
<div class="container">
${this.editableProperties.map(prop => this.#renderProperty(prop))}
</div>
`, this.#shadow, {
host: this,
});
// clang-format on
}
#renderProperty(prop: EditableProperty): LitHtml.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,
});
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>`;
}
#renderButton(propertyValue: string, propertyName: string, selected: boolean = false): LitHtml.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': selected,
});
const values = {propertyName, propertyValue};
const title = selected ? i18nString(UIStrings.deselectButton, values) : i18nString(UIStrings.selectButton, values);
return html`<button title=${title} class=${classes} @click=${
(): void => this.#onButtonClick(propertyName, propertyValue, selected)}>
<${IconButton.Icon.Icon.litTagName} style=${transform} .data=${
{iconName: iconInfo.iconName, color: 'var(--icon-color)', width: '18px', height: '18px'} as
IconButton.Icon.IconWithName}></${IconButton.Icon.Icon.litTagName}>
</button>`;
}
#onButtonClick(propertyName: string, propertyValue: string, selected: boolean): void {
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 {
protected readonly editableProperties: EditableProperty[] = FlexboxEditableProperties;
protected findIcon(query: string, computedProperties: Map<string, string>): IconInfo|null {
return findFlexContainerIcon(query, computedProperties);
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-flexbox-editor', FlexboxEditor);
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLElementTagNameMap {
'devtools-flexbox-editor': FlexboxEditor;
}
}
export class GridEditor extends StylePropertyEditor {
protected readonly editableProperties: EditableProperty[] = GridEditableProperties;
protected findIcon(query: string, computedProperties: Map<string, string>): IconInfo|null {
return findGridContainerIcon(query, computedProperties);
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-grid-editor', GridEditor);
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLElementTagNameMap {
'devtools-grid-editor': GridEditor;
}
}
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: 'align-content',
propertyValues: [
'center',
'space-between',
'space-around',
'space-evenly',
'stretch',
],
},
{
propertyName: 'justify-content',
propertyValues: [
'center',
'start',
'end',
'space-between',
'space-around',
'space-evenly',
],
},
{
propertyName: 'align-items',
propertyValues: [
'center',
'start',
'end',
'stretch',
'baseline',
],
},
{
propertyName: 'justify-items',
propertyValues: [
'center',
'start',
'end',
'stretch',
],
},
];