1// Copyright (c) 2020 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
5import * as Common from '../common/common.js';
6import * as ComponentHelpers from '../component_helpers/component_helpers.js';
7import * as LitHtml from '../third_party/lit-html/lit-html.js';
8
9const ls = Common.ls;
10const getStyleSheets = ComponentHelpers.GetStylesheet.getStyleSheets;
11
12interface KeyboardModifiedEvent extends Event {
13  shiftKey: boolean;
14}
15
16export class FormatChangedEvent extends Event {
17  data: {format: string, text: string|null};
18
19  constructor(format: string, text: string|null) {
20    super('format-changed', {});
21    this.data = {format, text};
22  }
23}
24
25export class ColorSwatch extends HTMLElement {
26  private readonly shadow = this.attachShadow({mode: 'open'});
27  private tooltip: string = ls`Shift-click to change color format`;
28  private text: string|null = null;
29  private _color: Common.Color.Color|null = null;
30  private _format: string|null = null;
31
32  constructor() {
33    super();
34    this.shadow.adoptedStyleSheets = [
35      ...getStyleSheets('inline_editor/colorSwatch.css', {patchThemeSupport: false}),
36    ];
37  }
38
39  get color(): Common.Color.Color|null {
40    return this._color;
41  }
42
43  get format(): string|null {
44    return this._format;
45  }
46  /**
47   * Render this swatch given a color object or text to be parsed as a color.
48   * @param color The color object or string to use for this swatch.
49   * @param formatOrUseUserSetting Either the format to be used as a string, or true to auto-detect the user-set format.
50   * @param tooltip The tooltip to use on the swatch.
51   */
52  renderColor(color: Common.Color.Color|string, formatOrUseUserSetting?: string|boolean, tooltip?: string): void {
53    if (typeof color === 'string') {
54      this._color = Common.Color.Color.parse(color);
55      this.text = color;
56      if (!this._color) {
57        this.renderTextOnly();
58        return;
59      }
60    } else {
61      this._color = color;
62    }
63
64    if (typeof formatOrUseUserSetting === 'boolean' && formatOrUseUserSetting) {
65      this._format = Common.Settings.detectColorFormat(this._color);
66    } else if (typeof formatOrUseUserSetting === 'string') {
67      this._format = formatOrUseUserSetting;
68    } else {
69      this._format = this._color.format();
70    }
71
72    this.text = this._color.asString(this._format);
73
74    if (tooltip) {
75      this.tooltip = tooltip;
76    }
77
78    this.render();
79  }
80
81  private renderTextOnly() {
82    // Non-color values can be passed to the component (like 'none' from border style).
83    LitHtml.render(this.text, this.shadow, {eventContext: this});
84  }
85
86  private render() {
87    // Disabled until https://crbug.com/1079231 is fixed.
88    // clang-format off
89
90    // Note that we use a <slot> with a default value here to display the color text. Consumers of this component are
91    // free to append any content to replace what is being shown here.
92    // Note also that whitespace between nodes is removed on purpose to avoid pushing these elements apart. Do not
93    // re-format the HTML code.
94    LitHtml.render(
95      LitHtml.html`<span class="color-swatch" title="${this.tooltip}"><span class="color-swatch-inner"
96        style="background-color:${this.text};"
97        @click=${this.onClick}
98        @mousedown=${this.consume}
99        @dblclick=${this.consume}></span></span><slot><span>${this.text}</span></slot>`,
100      this.shadow, {eventContext: this});
101    // clang-format on
102  }
103
104  private onClick(e: KeyboardModifiedEvent) {
105    e.stopPropagation();
106
107    if (e.shiftKey) {
108      this.toggleNextFormat();
109      return;
110    }
111
112    this.dispatchEvent(new Event('swatch-click'));
113  }
114
115  private consume(e: Event) {
116    e.stopPropagation();
117  }
118
119  private toggleNextFormat() {
120    if (!this._color || !this._format) {
121      return;
122    }
123
124    let currentValue;
125    do {
126      this._format = nextColorFormat(this._color, this._format);
127      currentValue = this._color.asString(this._format);
128    } while (currentValue === this.text);
129
130    if (currentValue) {
131      this.text = currentValue;
132      this.render();
133
134      this.dispatchEvent(new FormatChangedEvent(this._format, this.text));
135    }
136  }
137}
138
139if (!customElements.get('devtools-color-swatch')) {
140  customElements.define('devtools-color-swatch', ColorSwatch);
141}
142
143declare global {
144  interface HTMLElementTagNameMap {
145    'devtools-color-swatch': ColorSwatch;
146  }
147}
148
149function nextColorFormat(color: Common.Color.Color, curFormat: string): string {
150  // The format loop is as follows:
151  // * original
152  // * rgb(a)
153  // * hsl(a)
154  // * nickname (if the color has a nickname)
155  // * shorthex (if has short hex)
156  // * hex
157  const cf = Common.Color.Format;
158
159  switch (curFormat) {
160    case cf.Original:
161      return !color.hasAlpha() ? cf.RGB : cf.RGBA;
162
163    case cf.RGB:
164    case cf.RGBA:
165      return !color.hasAlpha() ? cf.HSL : cf.HSLA;
166
167    case cf.HSL:
168    case cf.HSLA:
169      if (color.nickname()) {
170        return cf.Nickname;
171      }
172      return color.detectHEXFormat();
173
174    case cf.ShortHEX:
175      return cf.HEX;
176
177    case cf.ShortHEXA:
178      return cf.HEXA;
179
180    case cf.HEXA:
181    case cf.HEX:
182      return cf.Original;
183
184    case cf.Nickname:
185      return color.detectHEXFormat();
186
187    default:
188      return cf.RGBA;
189  }
190}
191