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