UNPKG

chrome-devtools-frontend

Version:
547 lines (473 loc) • 22.2 kB
// Copyright (c) 2015 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 Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as ColorPicker from '../../ui/legacy/components/color_picker/color_picker.js'; import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js'; import * as UI from '../../ui/legacy/legacy.js'; import type {StylePropertiesSection} from './StylePropertiesSection.js'; import type {StylePropertyTreeElement} from './StylePropertyTreeElement.js'; import type {StylesSidebarPane} from './StylesSidebarPane.js'; const UIStrings = { /** * @description Tooltip text for an icon that opens the cubic bezier editor, which is a tool that * allows the user to edit cubic-bezier CSS properties directly. */ openCubicBezierEditor: 'Open cubic bezier editor', /** * @description Tooltip text for an icon that opens shadow editor. The shadow editor is a tool * which allows the user to edit CSS shadow properties. */ openShadowEditor: 'Open shadow editor', } as const; const str_ = i18n.i18n.registerUIStrings('panels/elements/ColorSwatchPopoverIcon.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); interface BezierPopoverIconParams { treeElement: StylePropertyTreeElement; swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper; swatch: InlineEditor.Swatches.BezierSwatch; } export class BezierPopoverIcon { private treeElement: StylePropertyTreeElement; private readonly swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper; private swatch: InlineEditor.Swatches.BezierSwatch; private readonly boundBezierChanged: (event: Common.EventTarget.EventTargetEvent<string>) => void; private readonly boundOnScroll: (event: Event) => void; private bezierEditor?: InlineEditor.BezierEditor.BezierEditor; private scrollerElement?: Element; private originalPropertyText?: string|null; constructor({ treeElement, swatchPopoverHelper, swatch, }: BezierPopoverIconParams) { this.treeElement = treeElement; this.swatchPopoverHelper = swatchPopoverHelper; this.swatch = swatch; UI.Tooltip.Tooltip.install(this.swatch.iconElement(), i18nString(UIStrings.openCubicBezierEditor)); this.swatch.iconElement().addEventListener('click', this.iconClick.bind(this), false); this.swatch.iconElement().addEventListener('mousedown', (event: Event) => event.consume(), false); this.boundBezierChanged = this.bezierChanged.bind(this); this.boundOnScroll = this.onScroll.bind(this); } private iconClick(event: Event): void { event.consume(true); if (this.swatchPopoverHelper.isShowing()) { this.swatchPopoverHelper.hide(true); return; } const model = InlineEditor.AnimationTimingModel.AnimationTimingModel.parse(this.swatch.bezierText()) || InlineEditor.AnimationTimingModel.LINEAR_BEZIER; this.bezierEditor = new InlineEditor.BezierEditor.BezierEditor(model); this.bezierEditor.addEventListener(InlineEditor.BezierEditor.Events.BEZIER_CHANGED, this.boundBezierChanged); this.swatchPopoverHelper.show(this.bezierEditor, this.swatch.iconElement(), this.onPopoverHidden.bind(this)); this.scrollerElement = this.swatch.enclosingNodeOrSelfWithClass('style-panes-wrapper'); if (this.scrollerElement) { this.scrollerElement.addEventListener('scroll', this.boundOnScroll, false); } this.originalPropertyText = this.treeElement.property.propertyText; this.treeElement.parentPane().setEditingStyle(true); const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().propertyUILocation( this.treeElement.property, false /* forName */); if (uiLocation) { void Common.Revealer.reveal(uiLocation, true /* omitFocus */); } } private bezierChanged(event: Common.EventTarget.EventTargetEvent<string>): void { this.swatch.setBezierText(event.data); void this.treeElement.applyStyleText(this.treeElement.renderedPropertyText(), false); } private onScroll(_event: Event): void { this.swatchPopoverHelper.hide(true); } private onPopoverHidden(commitEdit: boolean): void { if (this.scrollerElement) { this.scrollerElement.removeEventListener('scroll', this.boundOnScroll, false); } if (this.bezierEditor) { this.bezierEditor.removeEventListener(InlineEditor.BezierEditor.Events.BEZIER_CHANGED, this.boundBezierChanged); } this.bezierEditor = undefined; const propertyText = commitEdit ? this.treeElement.renderedPropertyText() : this.originalPropertyText || ''; void this.treeElement.applyStyleText(propertyText, true); this.treeElement.parentPane().setEditingStyle(false); delete this.originalPropertyText; } } export const enum ColorSwatchPopoverIconEvents { COLOR_CHANGED = 'colorchanged', } export interface ColorSwatchPopoverIconEventTypes { [ColorSwatchPopoverIconEvents.COLOR_CHANGED]: Common.Color.Color; } export class ColorSwatchPopoverIcon extends Common.ObjectWrapper.ObjectWrapper<ColorSwatchPopoverIconEventTypes> { private treeElement: StylePropertyTreeElement; private readonly swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper; private swatch: InlineEditor.ColorSwatch.ColorSwatch; private contrastInfo: ColorPicker.ContrastInfo.ContrastInfo|null; private readonly boundSpectrumChanged: (event: Common.EventTarget.EventTargetEvent<string>) => void; private readonly boundOnScroll: (event: Event) => void; private spectrum?: ColorPicker.Spectrum.Spectrum; private scrollerElement?: Element; private originalPropertyText?: string|null; constructor( treeElement: StylePropertyTreeElement, swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper, swatch: InlineEditor.ColorSwatch.ColorSwatch) { super(); this.treeElement = treeElement; this.swatchPopoverHelper = swatchPopoverHelper; this.swatch = swatch; this.swatch.addEventListener(InlineEditor.ColorSwatch.ClickEvent.eventName, this.iconClick.bind(this)); this.contrastInfo = null; this.boundSpectrumChanged = this.spectrumChanged.bind(this); this.boundOnScroll = this.onScroll.bind(this); } private generateCSSVariablesPalette(): ColorPicker.Spectrum.Palette { const matchedStyles = this.treeElement.matchedStyles(); const style = this.treeElement.property.ownerStyle; const cssVariables = matchedStyles.availableCSSVariables(style); const colors = []; const colorNames = []; for (const cssVariable of cssVariables) { if (cssVariable === this.treeElement.property.name) { continue; } const value = matchedStyles.computeCSSVariable(style, cssVariable); if (!value) { continue; } const color = Common.Color.parse(value.value); if (!color) { continue; } colors.push(value.value); colorNames.push(cssVariable); } return {title: 'CSS Variables', mutable: false, matchUserFormat: true, colors, colorNames}; } setContrastInfo(contrastInfo: ColorPicker.ContrastInfo.ContrastInfo): void { this.contrastInfo = contrastInfo; } private iconClick(event: Event): void { event.consume(true); this.showPopover(); } async toggleEyeDropper(): Promise<void> { await this.spectrum?.toggleColorPicker(); } showPopover(): void { if (this.swatchPopoverHelper.isShowing()) { this.swatchPopoverHelper.hide(true); return; } const color = this.swatch.getColor(); if (!color) { return; } this.spectrum = new ColorPicker.Spectrum.Spectrum(this.contrastInfo); this.spectrum.setColor(color); this.spectrum.addPalette(this.generateCSSVariablesPalette()); this.spectrum.addEventListener(ColorPicker.Spectrum.Events.SIZE_CHANGED, this.spectrumResized, this); this.spectrum.addEventListener(ColorPicker.Spectrum.Events.COLOR_CHANGED, this.boundSpectrumChanged); this.swatchPopoverHelper.show(this.spectrum, this.swatch, this.onPopoverHidden.bind(this)); this.scrollerElement = this.swatch.enclosingNodeOrSelfWithClass('style-panes-wrapper'); if (this.scrollerElement) { this.scrollerElement.addEventListener('scroll', this.boundOnScroll, false); } this.originalPropertyText = this.treeElement.property.propertyText; this.treeElement.parentPane().setEditingStyle(true); const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().propertyUILocation( this.treeElement.property, false /* forName */); if (uiLocation) { void Common.Revealer.reveal(uiLocation, true /* omitFocus */); } UI.Context.Context.instance().setFlavor(ColorSwatchPopoverIcon, this); } private spectrumResized(): void { this.swatchPopoverHelper.reposition(); } private async spectrumChanged(event: Common.EventTarget.EventTargetEvent<string>): Promise<void> { const getColor = (colorText: string): Common.Color.Color|null => { const color = Common.Color.parse(colorText); const customProperty = this.spectrum?.colorName()?.startsWith('--') && `var(${this.spectrum.colorName()})`; if (!color || !customProperty) { return color; } if (color.is(Common.Color.Format.HEX) || color.is(Common.Color.Format.HEXA) || color.is(Common.Color.Format.RGB) || color.is(Common.Color.Format.RGBA)) { return new Common.Color.Legacy(color.rgba(), color.format(), customProperty); } if (color.is(Common.Color.Format.HSL)) { return new Common.Color.HSL(color.h, color.s, color.l, color.alpha, customProperty); } if (color.is(Common.Color.Format.HWB)) { return new Common.Color.HWB(color.h, color.w, color.b, color.alpha, customProperty); } if (color.is(Common.Color.Format.LCH)) { return new Common.Color.LCH(color.l, color.c, color.h, color.alpha, customProperty); } if (color.is(Common.Color.Format.OKLCH)) { return new Common.Color.Oklch(color.l, color.c, color.h, color.alpha, customProperty); } if (color.is(Common.Color.Format.LAB)) { return new Common.Color.Lab(color.l, color.a, color.b, color.alpha, customProperty); } if (color.is(Common.Color.Format.OKLAB)) { return new Common.Color.Oklab(color.l, color.a, color.b, color.alpha, customProperty); } if (color.is(Common.Color.Format.SRGB) || color.is(Common.Color.Format.SRGB_LINEAR) || color.is(Common.Color.Format.DISPLAY_P3) || color.is(Common.Color.Format.A98_RGB) || color.is(Common.Color.Format.PROPHOTO_RGB) || color.is(Common.Color.Format.REC_2020) || color.is(Common.Color.Format.XYZ) || color.is(Common.Color.Format.XYZ_D50) || color.is(Common.Color.Format.XYZ_D65)) { return new Common.Color.ColorFunction( color.colorSpace, color.p0, color.p1, color.p2, color.alpha, customProperty); } throw new Error(`Forgot to handle color format ${color.format()}`); }; const color = getColor(event.data); if (!color) { return; } this.swatch.renderColor(color); this.dispatchEventToListeners(ColorSwatchPopoverIconEvents.COLOR_CHANGED, color); } private onScroll(_event: Event): void { this.swatchPopoverHelper.hide(true); } private onPopoverHidden(commitEdit: boolean): void { if (this.scrollerElement) { this.scrollerElement.removeEventListener('scroll', this.boundOnScroll, false); } if (this.spectrum) { this.spectrum.removeEventListener(ColorPicker.Spectrum.Events.COLOR_CHANGED, this.boundSpectrumChanged); } this.spectrum = undefined; const propertyText = commitEdit ? this.treeElement.renderedPropertyText() : this.originalPropertyText || ''; void this.treeElement.applyStyleText(propertyText, true); this.treeElement.parentPane().setEditingStyle(false); delete this.originalPropertyText; UI.Context.Context.instance().setFlavor(ColorSwatchPopoverIcon, null); } } export const enum ShadowEvents { SHADOW_CHANGED = 'shadowChanged', } export interface ShadowEventTypes { [ShadowEvents.SHADOW_CHANGED]: InlineEditor.CSSShadowEditor.CSSShadowModel; } export class ShadowSwatchPopoverHelper extends Common.ObjectWrapper.ObjectWrapper<ShadowEventTypes> { private treeElement: StylePropertyTreeElement; private readonly swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper; private readonly shadowSwatch: InlineEditor.Swatches.CSSShadowSwatch; private iconElement: HTMLSpanElement; private readonly boundShadowChanged: (event: Common.EventTarget.EventTargetEvent<InlineEditor.CSSShadowEditor.CSSShadowModel>) => void; private readonly boundOnScroll: (event: Event) => void; private cssShadowEditor?: InlineEditor.CSSShadowEditor.CSSShadowEditor; private scrollerElement?: Element; private originalPropertyText?: string|null; constructor( treeElement: StylePropertyTreeElement, swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper, shadowSwatch: InlineEditor.Swatches.CSSShadowSwatch) { super(); this.treeElement = treeElement; this.swatchPopoverHelper = swatchPopoverHelper; this.shadowSwatch = shadowSwatch; this.iconElement = shadowSwatch.iconElement(); UI.Tooltip.Tooltip.install(this.iconElement, i18nString(UIStrings.openShadowEditor)); this.iconElement.addEventListener('click', this.iconClick.bind(this), false); this.iconElement.addEventListener('keydown', this.keyDown.bind(this), false); this.iconElement.addEventListener('mousedown', event => event.consume(), false); this.boundShadowChanged = this.shadowChanged.bind(this); this.boundOnScroll = this.onScroll.bind(this); } private keyDown(event: KeyboardEvent): void { if (Platform.KeyboardUtilities.isEnterOrSpaceKey(event)) { event.consume(true); this.showPopover(); } } private iconClick(event: Event): void { event.consume(true); this.showPopover(); } showPopover(): void { if (this.swatchPopoverHelper.isShowing()) { this.swatchPopoverHelper.hide(true); return; } this.cssShadowEditor = new InlineEditor.CSSShadowEditor.CSSShadowEditor(); this.cssShadowEditor.element.classList.toggle('with-padding', true); this.cssShadowEditor.setModel(this.shadowSwatch.model()); this.cssShadowEditor.addEventListener(InlineEditor.CSSShadowEditor.Events.SHADOW_CHANGED, this.boundShadowChanged); this.swatchPopoverHelper.show(this.cssShadowEditor, this.iconElement, this.onPopoverHidden.bind(this)); this.scrollerElement = this.iconElement.enclosingNodeOrSelfWithClass('style-panes-wrapper'); if (this.scrollerElement) { this.scrollerElement.addEventListener('scroll', this.boundOnScroll, false); } this.originalPropertyText = this.treeElement.property.propertyText; this.treeElement.parentPane().setEditingStyle(true); const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().propertyUILocation( this.treeElement.property, false /* forName */); if (uiLocation) { void Common.Revealer.reveal(uiLocation, true /* omitFocus */); } } private shadowChanged(event: Common.EventTarget.EventTargetEvent<InlineEditor.CSSShadowEditor.CSSShadowModel>): void { this.dispatchEventToListeners(ShadowEvents.SHADOW_CHANGED, event.data); } private onScroll(_event: Event): void { this.swatchPopoverHelper.hide(true); } private onPopoverHidden(commitEdit: boolean): void { if (this.scrollerElement) { this.scrollerElement.removeEventListener('scroll', this.boundOnScroll, false); } if (this.cssShadowEditor) { this.cssShadowEditor.removeEventListener( InlineEditor.CSSShadowEditor.Events.SHADOW_CHANGED, this.boundShadowChanged); } this.cssShadowEditor = undefined; const propertyText = commitEdit ? this.treeElement.renderedPropertyText() : this.originalPropertyText || ''; void this.treeElement.applyStyleText(propertyText, true); this.treeElement.parentPane().setEditingStyle(false); delete this.originalPropertyText; } } export class FontEditorSectionManager { private readonly treeElementMap: Map<string, StylePropertyTreeElement>; private readonly swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper; private readonly section: StylePropertiesSection; private parentPane: StylesSidebarPane|null; private fontEditor: InlineEditor.FontEditor.FontEditor|null; private scrollerElement: Element|null; private readonly boundFontChanged: (event: Common.EventTarget.EventTargetEvent<InlineEditor.FontEditor.FontChangedEvent>) => void; private readonly boundOnScroll: () => void; private readonly boundResized: () => void; constructor( swatchPopoverHelper: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper, section: StylePropertiesSection) { this.treeElementMap = new Map(); this.swatchPopoverHelper = swatchPopoverHelper; this.section = section; this.parentPane = null; this.fontEditor = null; this.scrollerElement = null; this.boundFontChanged = this.fontChanged.bind(this); this.boundOnScroll = this.onScroll.bind(this); this.boundResized = this.fontEditorResized.bind(this); } private fontChanged(event: Common.EventTarget.EventTargetEvent<InlineEditor.FontEditor.FontChangedEvent>): void { const {propertyName, value} = event.data; const treeElement = this.treeElementMap.get(propertyName); void this.updateFontProperty(propertyName, value, treeElement); } private async updateFontProperty(propertyName: string, value: string, treeElement?: StylePropertyTreeElement): Promise<void> { if (treeElement?.treeOutline && treeElement.valueElement && treeElement.property.parsedOk && treeElement.property.range) { let elementRemoved = false; treeElement.valueElement.textContent = value; treeElement.property.value = value; let styleText; const propertyName = treeElement.property.name; if (value.length) { styleText = treeElement.renderedPropertyText(); } else { styleText = ''; elementRemoved = true; this.fixIndex(treeElement.property.index); } this.treeElementMap.set(propertyName, treeElement); await treeElement.applyStyleText(styleText, true); if (elementRemoved) { this.treeElementMap.delete(propertyName); } } else if (value.length) { const newProperty = this.section.addNewBlankProperty(); if (newProperty) { newProperty.property.name = propertyName; newProperty.property.value = value; newProperty.updateTitle(); await newProperty.applyStyleText(newProperty.renderedPropertyText(), true); this.treeElementMap.set(newProperty.property.name, newProperty); } } this.section.onpopulate(); this.swatchPopoverHelper.reposition(); return; } private fontEditorResized(): void { this.swatchPopoverHelper.reposition(); } private fixIndex(removedIndex: number): void { for (const treeElement of this.treeElementMap.values()) { if (treeElement.property.index > removedIndex) { treeElement.property.index -= 1; } } } private createPropertyValueMap(): Map<string, string> { const propertyMap = new Map<string, string>(); for (const fontProperty of this.treeElementMap) { const propertyName = (fontProperty[0]); const treeElement = fontProperty[1]; if (treeElement.property.value.length) { propertyMap.set(propertyName, treeElement.property.value); } else { this.treeElementMap.delete(propertyName); } } return propertyMap; } registerFontProperty(treeElement: StylePropertyTreeElement): void { const propertyName = treeElement.property.name; if (this.treeElementMap.has(propertyName)) { const treeElementFromMap = this.treeElementMap.get(propertyName); if (!treeElement.overloaded() || (treeElementFromMap?.overloaded())) { this.treeElementMap.set(propertyName, treeElement); } } else { this.treeElementMap.set(propertyName, treeElement); } } async showPopover(iconElement: Element, parentPane: StylesSidebarPane): Promise<void> { if (this.swatchPopoverHelper.isShowing()) { this.swatchPopoverHelper.hide(true); return; } this.parentPane = parentPane; const propertyValueMap = this.createPropertyValueMap(); this.fontEditor = new InlineEditor.FontEditor.FontEditor(propertyValueMap); this.fontEditor.addEventListener(InlineEditor.FontEditor.Events.FONT_CHANGED, this.boundFontChanged); this.fontEditor.addEventListener(InlineEditor.FontEditor.Events.FONT_EDITOR_RESIZED, this.boundResized); this.swatchPopoverHelper.show(this.fontEditor, iconElement, this.onPopoverHidden.bind(this)); this.scrollerElement = iconElement.enclosingNodeOrSelfWithClass('style-panes-wrapper'); if (this.scrollerElement) { this.scrollerElement.addEventListener('scroll', this.boundOnScroll, false); } this.parentPane.setEditingStyle(true); } private onScroll(): void { this.swatchPopoverHelper.hide(true); } private onPopoverHidden(): void { if (this.scrollerElement) { this.scrollerElement.removeEventListener('scroll', this.boundOnScroll, false); } this.section.onpopulate(); if (this.fontEditor) { this.fontEditor.removeEventListener(InlineEditor.FontEditor.Events.FONT_CHANGED, this.boundFontChanged); } this.fontEditor = null; if (this.parentPane) { this.parentPane.setEditingStyle(false); } this.section.resetToolbars(); this.section.onpopulate(); } }