| // Copyright 2023 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/legacy/legacy.js'; |
| |
| import * as Common from '../../../core/common/common.js'; |
| import * as i18n from '../../../core/i18n/i18n.js'; |
| import type * as Platform from '../../../core/platform/platform.js'; |
| import {html, nothing, render} from '../../../ui/lit/lit.js'; |
| import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; |
| |
| import CSSPropertyDocsViewStyles from './cssPropertyDocsView.css.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Text for button that redirects to CSS property documentation. |
| */ |
| learnMore: 'Learn more', |
| /** |
| * @description Text for a checkbox to turn off the CSS property documentation. |
| */ |
| dontShow: 'Don\'t show', |
| /** |
| * @description Text indicating that the CSS property has limited availability across major browsers. |
| */ |
| limitedAvailability: 'Limited availability across major browsers', |
| /** |
| * @description Text indicating that the CSS property has limited availability across major browsers, with a list of unsupported browsers. |
| * @example {Firefox} PH1 |
| * @example {Safari on iOS} PH1 |
| * @example {Chrome, Firefox on Android, or Safari} PH1 |
| */ |
| limitedAvailabilityInBrowsers: 'Limited availability across major browsers (not fully implemented in {PH1})', |
| /** |
| * @description Text to display a combination of browser name and platform name. |
| * @example {Safari} PH1 |
| * @example {iOS} PH2 |
| */ |
| browserOnPlatform: '{PH1} on {PH2}', |
| /** |
| * @description Text indicating that the CSS property is newly available across major browsers since a certain time. |
| * @example {September 2015} PH1 |
| */ |
| newlyAvailableSince: 'Newly available across major browsers (`Baseline` since {PH1})', |
| /** |
| * @description Text indicating that the CSS property is widely available across major browsers since a certain time. |
| * @example {September 2015} PH1 |
| * @example {an unknown date} PH1 |
| */ |
| widelyAvailableSince: 'Widely available across major browsers (`Baseline` since {PH1})', |
| /** |
| * @description Text indicating that a specific date is not known. |
| */ |
| unknownDate: 'an unknown date', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('panels/elements/components/CSSPropertyDocsView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| const BASELINE_HIGH_AVAILABILITY_ICON = |
| '../../../Images/baseline-high-availability.svg' as Platform.DevToolsPath.RawPathString; |
| const BASELINE_LOW_AVAILABILITY_ICON = |
| '../../../Images/baseline-low-availability.svg' as Platform.DevToolsPath.RawPathString; |
| const BASELINE_LIMITED_AVAILABILITY_ICON = |
| '../../../Images/baseline-limited-availability.svg' as Platform.DevToolsPath.RawPathString; |
| |
| const getBaselineIconPath = (baseline: Baseline): Platform.DevToolsPath.UrlString => { |
| let relativePath: string; |
| switch (baseline.status) { |
| case BaselineStatus.HIGH: |
| relativePath = BASELINE_HIGH_AVAILABILITY_ICON; |
| break; |
| case BaselineStatus.LOW: |
| relativePath = BASELINE_LOW_AVAILABILITY_ICON; |
| break; |
| default: |
| relativePath = BASELINE_LIMITED_AVAILABILITY_ICON; |
| } |
| return new URL(relativePath, import.meta.url).toString() as Platform.DevToolsPath.UrlString; |
| }; |
| |
| const enum BrowserId { |
| C = 'C', |
| CA = 'CA', |
| E = 'E', |
| FF = 'FF', |
| FFA = 'FFA', |
| S = 'S', |
| SM = 'SM', |
| } |
| const allBrowserIds = new Set<BrowserId>( |
| [BrowserId.C, BrowserId.CA, BrowserId.E, BrowserId.FF, BrowserId.FFA, BrowserId.S, BrowserId.SM]); |
| const enum BrowserPlatform { |
| DESKTOP = 'desktop', |
| ANDROID = 'Android', |
| MACOS = 'macOS', |
| IOS = 'iOS', |
| } |
| const browserIdToNameAndPlatform = new Map([ |
| [BrowserId.C, {name: 'Chrome', platform: BrowserPlatform.DESKTOP}], |
| [BrowserId.CA, {name: 'Chrome', platform: BrowserPlatform.ANDROID}], |
| [BrowserId.E, {name: 'Edge', platform: BrowserPlatform.DESKTOP}], |
| [BrowserId.FF, {name: 'Firefox', platform: BrowserPlatform.DESKTOP}], |
| [BrowserId.FFA, {name: 'Firefox', platform: BrowserPlatform.ANDROID}], |
| [BrowserId.S, {name: 'Safari', platform: BrowserPlatform.MACOS}], |
| [BrowserId.SM, {name: 'Safari', platform: BrowserPlatform.IOS}], |
| ]); |
| |
| function formatBrowserList(browserNames: Map<string, BrowserPlatform[]>): string { |
| const formatter = new Intl.ListFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, { |
| style: 'long', |
| type: 'disjunction', |
| }); |
| return formatter.format(browserNames.entries().map( |
| ([name, platforms]) => platforms.length !== 1 || platforms[0] === BrowserPlatform.DESKTOP ? |
| name : |
| i18nString(UIStrings.browserOnPlatform, {PH1: name, PH2: platforms[0]}))); |
| } |
| |
| const formatBaselineDate = (date?: string): string => { |
| if (!date) { |
| return i18nString(UIStrings.unknownDate); |
| } |
| const parsedDate = new Date(date); |
| const formatter = new Intl.DateTimeFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, { |
| month: 'long', |
| year: 'numeric', |
| }); |
| return formatter.format(parsedDate); |
| }; |
| |
| const getBaselineMissingBrowsers = (browsers: string[]): Map<string, BrowserPlatform[]> => { |
| const browserIds = browsers.map(v => v.replace(/\d*$/, '')); |
| const missingBrowserIds = allBrowserIds.difference(new Set(browserIds)); |
| const missingBrowsers = new Map(); |
| for (const id of missingBrowserIds) { |
| const browserInfo = browserIdToNameAndPlatform.get(id); |
| if (browserInfo) { |
| const {name, platform} = browserInfo; |
| missingBrowsers.set(name, [...(missingBrowsers.get(name) ?? []), platform]); |
| } |
| } |
| return missingBrowsers; |
| }; |
| |
| const getBaselineText = (baseline: Baseline, browsers?: string[]): string => { |
| if (baseline.status === BaselineStatus.LIMITED) { |
| const missingBrowsers = browsers && getBaselineMissingBrowsers(browsers); |
| if (missingBrowsers) { |
| return i18nString(UIStrings.limitedAvailabilityInBrowsers, {PH1: formatBrowserList(missingBrowsers)}); |
| } |
| return i18nString(UIStrings.limitedAvailability); |
| } |
| |
| if (baseline.status === BaselineStatus.LOW) { |
| return i18nString(UIStrings.newlyAvailableSince, {PH1: formatBaselineDate(baseline.baseline_low_date)}); |
| } |
| return i18nString(UIStrings.widelyAvailableSince, {PH1: formatBaselineDate(baseline.baseline_high_date)}); |
| }; |
| |
| export const enum BaselineStatus { |
| LIMITED = 'false', |
| LOW = 'low', |
| HIGH = 'high', |
| } |
| |
| /** |
| * An object containing Baseline (https://web.dev/baseline, |
| * https://web-platform-dx.github.io/web-features/) information about the feature's browser |
| * compatibility. |
| */ |
| interface Baseline { |
| /** |
| * The Baseline status of the feature: |
| * - `"false"` — limited availability across major browsers |
| * - `"low"` — newly available across major browsers |
| * - `"high"` — widely available across major browsers |
| */ |
| status: BaselineStatus; |
| /** |
| * A date in the format `YYYY-MM-DD` representing when the feature became newly available, |
| * or `undefined` if it hasn't yet reached that status. |
| */ |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| baseline_low_date?: string; |
| /** |
| * A date in the format `YYYY-MM-DD` representing when the feature became widely available, |
| * or `undefined` if it hasn't yet reached that status. |
| * |
| * The widely available date is always 30 months after the newly available date. |
| */ |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| baseline_high_date?: string; |
| } |
| |
| export interface CSSProperty { |
| name: string; |
| description?: string; |
| baseline?: Baseline; |
| browsers?: string[]; |
| references?: Array<{ |
| name: string, |
| url: string, |
| }>; |
| } |
| |
| export class CSSPropertyDocsView extends HTMLElement { |
| readonly #shadow = this.attachShadow({mode: 'open'}); |
| readonly #cssProperty: CSSProperty; |
| |
| constructor(cssProperty: CSSProperty) { |
| super(); |
| this.#cssProperty = cssProperty; |
| this.#render(); |
| } |
| |
| #dontShowChanged(e: Event): void { |
| const showDocumentation = !(e.target as HTMLInputElement).checked; |
| Common.Settings.Settings.instance() |
| .moduleSetting('show-css-property-documentation-on-hover') |
| .set(showDocumentation); |
| } |
| |
| #render(): void { |
| const {description, references, baseline, browsers} = this.#cssProperty; |
| const link = references?.[0].url; |
| |
| // Disabled until https://crbug.com/1079231 is fixed. |
| // clang-format off |
| render(html` |
| <style>${CSSPropertyDocsViewStyles}</style> |
| <div class="docs-popup-wrapper"> |
| ${description ? html` |
| <div id="description"> |
| ${description} |
| </div> |
| ` : nothing} |
| ${baseline ? html` |
| <div id="baseline" class="docs-popup-section"> |
| <img |
| id="baseline-icon" |
| src=${getBaselineIconPath(baseline)} |
| role="presentation" |
| > |
| <span> |
| ${getBaselineText(baseline, browsers)} |
| </span> |
| </div> |
| ` : nothing} |
| ${link ? html` |
| <div class="docs-popup-section footer"> |
| <x-link |
| id="learn-more" |
| href=${link} |
| class="clickable underlined unbreakable-text" |
| > |
| ${i18nString(UIStrings.learnMore)} |
| </x-link> |
| <devtools-checkbox |
| @change=${this.#dontShowChanged} |
| jslog=${VisualLogging.toggle('css-property-doc').track({ change: true })}> |
| ${i18nString(UIStrings.dontShow)} |
| </devtools-checkbox> |
| </div> |
| ` : nothing} |
| </div> |
| `, this.#shadow, { |
| host: this, |
| }); |
| // clang-format on |
| } |
| } |
| |
| customElements.define('devtools-css-property-docs-view', CSSPropertyDocsView); |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'devtools-css-property-docs-view': CSSPropertyDocsView; |
| } |
| } |