diff --git a/change/@microsoft-fast-element-06c5f5c1-f172-4ba0-864e-9d9b494adda5.json b/change/@microsoft-fast-element-06c5f5c1-f172-4ba0-864e-9d9b494adda5.json new file mode 100644 index 00000000000..b37308369e8 --- /dev/null +++ b/change/@microsoft-fast-element-06c5f5c1-f172-4ba0-864e-9d9b494adda5.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "refactor: extract polyfill and polyfill-like code to an optional module", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-foundation-e05243ea-6de7-4694-abe1-2c4b64a25077.json b/change/@microsoft-fast-foundation-e05243ea-6de7-4694-abe1-2c4b64a25077.json new file mode 100644 index 00000000000..9532bc8d531 --- /dev/null +++ b/change/@microsoft-fast-foundation-e05243ea-6de7-4694-abe1-2c4b64a25077.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update templates to use classList and fix classList bug", + "packageName": "@microsoft/fast-foundation", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 80789753aad..1f07eca41b9 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -4,9 +4,6 @@ ```ts -// @public -export const $global: Global; - // @public export interface Accessor { getValue(source: any): any; @@ -14,9 +11,31 @@ export interface Accessor { setValue(source: any, value: any): void; } +// Warning: (ae-internal-missing-underscore) The name "AdoptedStyleSheetsStrategy" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export class AdoptedStyleSheetsStrategy implements StyleStrategy { + constructor(styles: (string | CSSStyleSheet)[]); + // (undocumented) + addStylesTo(target: StyleTarget): void; + // (undocumented) + removeStylesFrom(target: StyleTarget): void; + // (undocumented) + readonly sheets: CSSStyleSheet[]; +} + +// @public +export enum Aspect { + attribute = 0, + booleanAttribute = 1, + content = 3, + event = 5, + property = 2, + tokenList = 4 +} + // @public export abstract class AspectedHTMLDirective extends HTMLDirective { - // Warning: (ae-forgotten-export) The symbol "Aspect" needs to be exported by the entry point index.d.ts abstract readonly aspect: Aspect; abstract readonly binding?: Binding; abstract captureSource(source: string): void; @@ -117,7 +136,7 @@ export interface ChildListDirectiveOptions extends NodeBehaviorOptions< // @public export function children(propertyOrOptions: (keyof T & string) | ChildListDirectiveOptions): CaptureType; -// Warning: (ae-forgotten-export) The symbol "NodeObservationDirective" needs to be exported by the entry point index.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "ChildrenDirective" is marked as @public, but its signature references "NodeObservationDirective" which is marked as @internal // // @public export class ChildrenDirective extends NodeObservationDirective { @@ -137,6 +156,7 @@ directives: readonly HTMLDirective[]) => HTMLTemplateCompilationResult; // @public export const Compiler: { + setHTMLPolicy(policy: TrustedTypesPolicy): void; compile(html: string | HTMLTemplateElement, directives: ReadonlyArray): HTMLTemplateCompilationResult; setDefaultStrategy(strategy: CompilationStrategy): void; aggregate(parts: (string | HTMLDirective)[]): HTMLDirective; @@ -207,8 +227,6 @@ export const defaultExecutionContext: ExecutionContext; // @public export const DOM: Readonly<{ supportsAdoptedStyleSheets: boolean; - setHTMLPolicy(policy: TrustedTypesPolicy): void; - createHTML(html: string): string; setUpdateMode: (isAsync: boolean) => void; queueUpdate: (callable: Callable) => void; nextUpdate(): Promise; @@ -328,12 +346,6 @@ export interface FASTGlobal { readonly versions: string[]; } -// @public -export type Global = typeof globalThis & { - trustedTypes: TrustedTypes; - readonly FAST: FASTGlobal; -}; - // @public export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; @@ -366,20 +378,6 @@ export class HTMLView implemen unbind(): void; } -// Warning: (ae-internal-missing-underscore) The name "KernelServiceId" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const enum KernelServiceId { - // (undocumented) - contextEvent = 3, - // (undocumented) - elementRegistry = 4, - // (undocumented) - observable = 2, - // (undocumented) - updateQueue = 1 -} - // @public export const Markup: Readonly<{ interpolation: (index: number) => string; @@ -400,6 +398,19 @@ export interface NodeBehaviorOptions { property: T; } +// Warning: (ae-internal-missing-underscore) The name "NodeObservationDirective" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export abstract class NodeObservationDirective extends StatelessAttachedAttributeDirective { + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; + protected computeNodes(target: any): Node[]; + protected abstract disconnect(target: any): void; + protected abstract getNodes(target: any): Node[]; + protected abstract observe(target: any): void; + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; + protected updateTarget(source: any, value: ReadonlyArray): void; +} + // @public export interface Notifier { notify(args: any): void; @@ -468,8 +479,6 @@ export class PropertyChangeNotifier implements Notifier { // @public export const ref: (propertyName: keyof T & string) => CaptureType; -// Warning: (ae-forgotten-export) The symbol "StatelessAttachedAttributeDirective" needs to be exported by the entry point index.d.ts -// // @public export class RefDirective extends StatelessAttachedAttributeDirective { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; @@ -504,6 +513,8 @@ export interface RepeatOptions { // @public export function slotted(propertyOrOptions: (keyof T & string) | SlottedDirectiveOptions): CaptureType; +// Warning: (ae-incompatible-release-tags) The symbol "SlottedDirective" is marked as @public, but its signature references "NodeObservationDirective" which is marked as @internal +// // @public export class SlottedDirective extends NodeObservationDirective { disconnect(target: EventSource): void; @@ -530,6 +541,17 @@ export class Splice { removed: any[]; } +// @public +export abstract class StatelessAttachedAttributeDirective extends HTMLDirective implements ViewBehavior { + constructor(options: T); + abstract bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; + createPlaceholder: (index: number) => string; + // (undocumented) + protected options: T; + abstract unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; +} + // @public export interface StyleStrategy { addStylesTo(target: StyleTarget): void; diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 521d2457a9a..c2d2d3c0f75 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -1,8 +1,8 @@ -import type { Mutable } from "../interfaces.js"; +import type { Mutable, StyleTarget } from "../interfaces.js"; import type { Behavior } from "../observation/behavior.js"; import { PropertyChangeNotifier } from "../observation/notifier.js"; import { defaultExecutionContext, Observable } from "../observation/observable.js"; -import type { ElementStyles, StyleTarget } from "../styles/element-styles.js"; +import type { ElementStyles } from "../styles/element-styles.js"; import type { ElementViewTemplate } from "../templating/template.js"; import type { ElementView } from "../templating/view.js"; import { FASTElementDefinition } from "./fast-definitions.js"; diff --git a/packages/web-components/fast-element/src/components/fast-definitions.ts b/packages/web-components/fast-element/src/components/fast-definitions.ts index 0b8737063ab..275dd93b7c6 100644 --- a/packages/web-components/fast-element/src/components/fast-definitions.ts +++ b/packages/web-components/fast-element/src/components/fast-definitions.ts @@ -1,6 +1,6 @@ -import { isString } from "../interfaces.js"; +import { isString, KernelServiceId } from "../interfaces.js"; import { Observable } from "../observation/observable.js"; -import { FAST, KernelServiceId } from "../platform.js"; +import { FAST } from "../platform.js"; import { ComposableStyles, ElementStyles } from "../styles/element-styles.js"; import type { ElementViewTemplate } from "../templating/template.js"; import { AttributeConfiguration, AttributeDefinition } from "./attributes.js"; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index c4686a1a8eb..ebcc1f9fcdd 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -1,19 +1,10 @@ -import type { Callable } from "./interfaces.js"; -import { $global, KernelServiceId, TrustedTypesPolicy } from "./platform.js"; - -/* eslint-disable */ -const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( - "fast-html", - { - createHTML: html => html, - } -); -/* eslint-enable */ +import { Callable, KernelServiceId } from "./interfaces.js"; +import { FAST } from "./platform.js"; -const updateQueue = $global.FAST.getById(KernelServiceId.updateQueue, () => { +const updateQueue = FAST.getById(KernelServiceId.updateQueue, () => { const tasks: Callable[] = []; const pendingErrors: any[] = []; - const rAF = $global.requestAnimationFrame; + const rAF = globalThis.requestAnimationFrame; let updateAsync = true; function throwFirstError(): void { @@ -85,8 +76,6 @@ const updateQueue = $global.FAST.getById(KernelServiceId.updateQueue, () => { }); }); -let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy; - /** * Common DOM APIs. * @public @@ -99,32 +88,6 @@ export const DOM = Object.freeze({ Array.isArray((document as any).adoptedStyleSheets) && "replace" in CSSStyleSheet.prototype, - /** - * Sets the HTML trusted types policy used by the templating engine. - * @param policy - The policy to set for HTML. - * @remarks - * This API can only be called once, for security reasons. It should be - * called by the application developer at the start of their program. - */ - setHTMLPolicy(policy: TrustedTypesPolicy) { - if (htmlPolicy !== fastHTMLPolicy) { - throw new Error("The HTML policy can only be set once."); - } - - htmlPolicy = policy; - }, - - /** - * Turns a string into trusted HTML using the configured trusted types policy. - * @param html - The string to turn into trusted HTML. - * @remarks - * Used internally by the template engine when creating templates - * and setting innerHTML. - */ - createHTML(html: string): string { - return htmlPolicy.createHTML(html); - }, - /** * Sets the update mode used by queueUpdate. * @param isAsync - Indicates whether DOM updates should be asynchronous. diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index a55722f21a6..9e0a9ac3e61 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -1,28 +1,28 @@ export * from "./platform.js"; export * from "./templating/template.js"; export * from "./components/fast-element.js"; -export { - FASTElementDefinition, - PartialFASTElementDefinition, -} from "./components/fast-definitions.js"; +export * from "./components/fast-definitions.js"; export * from "./components/attributes.js"; export * from "./components/controller.js"; -export type { Callable, Constructable, Mutable } from "./interfaces.js"; -export * from "./templating/compiler.js"; -export { - ElementStyles, +export type { + Callable, + Constructable, + FASTGlobal, + Mutable, StyleStrategy, - ConstructibleStyleStrategy, - ComposableStyles, StyleTarget, -} from "./styles/element-styles.js"; -export { css, cssPartial } from "./styles/css.js"; -export { CSSDirective } from "./styles/css-directive.js"; + TrustedTypes, + TrustedTypesPolicy, +} from "./interfaces.js"; +export * from "./templating/compiler.js"; +export * from "./styles/element-styles.js"; +export * from "./styles/css.js"; +export * from "./styles/css-directive.js"; export * from "./observation/observable.js"; export * from "./observation/notifier.js"; -export { Splice } from "./observation/array-change-records.js"; -export { enableArrayObservation } from "./observation/array-observer.js"; -export { DOM } from "./dom.js"; +export * from "./observation/array-change-records.js"; +export * from "./observation/array-observer.js"; +export * from "./dom.js"; export type { Behavior } from "./observation/behavior.js"; export { Markup, Parser } from "./templating/markup.js"; export { @@ -35,21 +35,11 @@ export { BindingBehaviorFactory, DefaultBindingOptions, } from "./templating/binding.js"; -export { - ViewBehaviorTargets, - ViewBehavior, - ViewBehaviorFactory, - HTMLDirective, - AspectedHTMLDirective, -} from "./templating/html-directive.js"; +export * from "./templating/html-directive.js"; export * from "./templating/ref.js"; export * from "./templating/when.js"; export * from "./templating/repeat.js"; export * from "./templating/slotted.js"; export * from "./templating/children.js"; export * from "./templating/view.js"; -export { - elements, - ElementsFilter, - NodeBehaviorOptions, -} from "./templating/node-observation.js"; +export * from "./templating/node-observation.js"; diff --git a/packages/web-components/fast-element/src/interfaces.ts b/packages/web-components/fast-element/src/interfaces.ts index 4a7991af745..59da7930884 100644 --- a/packages/web-components/fast-element/src/interfaces.ts +++ b/packages/web-components/fast-element/src/interfaces.ts @@ -21,6 +21,110 @@ export type Mutable = { -readonly [P in keyof T]: T[P]; }; +/** + * A policy for use with the standard trustedTypes platform API. + * @public + */ +export type TrustedTypesPolicy = { + /** + * Creates trusted HTML. + * @param html - The HTML to clear as trustworthy. + */ + createHTML(html: string): string; +}; + +/** + * Enables working with trusted types. + * @public + */ +export type TrustedTypes = { + /** + * Creates a trusted types policy. + * @param name - The policy name. + * @param rules - The policy rules implementation. + */ + createPolicy(name: string, rules: TrustedTypesPolicy): TrustedTypesPolicy; +}; + +/** + * The FAST global. + * @internal + */ +export interface FASTGlobal { + /** + * The list of loaded versions. + */ + readonly versions: string[]; + + /** + * Gets a kernel value. + * @param id - The id to get the value for. + * @param initialize - Creates the initial value for the id if not already existing. + */ + getById(id: string | number): T | null; + getById(id: string | number, initialize: () => T): T; +} + +/** + * Core services shared across FAST instances. + * @internal + */ +export const enum KernelServiceId { + updateQueue = 1, + observable = 2, + contextEvent = 3, + elementRegistry = 4, + styleSheetStrategy = 5, +} + +/** + * A node that can be targeted by styles. + * @public + */ +export interface StyleTarget { + /** + * Stylesheets to be adopted by the node. + */ + adoptedStyleSheets?: CSSStyleSheet[]; + + /** + * Adds styles to the target by appending the styles. + * @param styles - The styles element to add. + */ + append(styles: HTMLStyleElement): void; + + /** + * Removes styles from the target. + * @param styles - The styles element to remove. + */ + removeChild(styles: HTMLStyleElement): void; + + /** + * Returns all element descendants of node that match selectors. + * @param selectors - The CSS selector to use for the query. + */ + querySelectorAll(selectors: string): NodeListOf; +} + +/** + * Implemented to provide specific behavior when adding/removing styles + * for elements. + * @public + */ +export interface StyleStrategy { + /** + * Adds styles to the target. + * @param target - The target to add the styles to. + */ + addStylesTo(target: StyleTarget): void; + + /** + * Removes styles from the target. + * @param target - The target to remove the styles from. + */ + removeStylesFrom(target: StyleTarget): void; +} + /** * @internal */ diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index ad52f93d02d..51310da5801 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -4,7 +4,7 @@ import { SubscriberSet } from "./notifier.js"; import type { Notifier } from "./notifier.js"; import { Observable } from "./observable.js"; -function setNonEnumerable(target: any, property: string, value: any) { +function setNonEnumerable(target: any, property: string, value: any): void { Reflect.defineProperty(target, property, { value, enumerable: false, @@ -95,8 +95,7 @@ export function enableArrayObservation(): void { const sort = proto.sort; const splice = proto.splice; const unshift = proto.unshift; - - function adjustIndex(changeRecord: Splice, array: any[]): Splice { + const adjustIndex = (changeRecord: Splice, array: any[]): Splice => { let index = changeRecord.index; const arrayLength = array.length; @@ -112,7 +111,7 @@ export function enableArrayObservation(): void { changeRecord.index = index < 0 ? 0 : index; return changeRecord; - } + }; Object.assign(proto, { pop(...args) { diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index 01bee669fb7..e07c0571839 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -1,6 +1,6 @@ import { DOM } from "../dom.js"; -import { isFunction, isString } from "../interfaces.js"; -import { FAST, KernelServiceId } from "../platform.js"; +import { isFunction, isString, KernelServiceId } from "../interfaces.js"; +import { FAST } from "../platform.js"; import { PropertyChangeNotifier, SubscriberSet } from "./notifier.js"; import type { Notifier, Subscriber } from "./notifier.js"; diff --git a/packages/web-components/fast-element/src/platform.spec.ts b/packages/web-components/fast-element/src/platform.spec.ts index b5b51ec74fd..4e0d7f4e1b7 100644 --- a/packages/web-components/fast-element/src/platform.spec.ts +++ b/packages/web-components/fast-element/src/platform.spec.ts @@ -1,5 +1,7 @@ import { expect } from "chai"; -import { FAST } from './platform'; +import type { FASTGlobal } from "./interfaces"; + +declare const FAST: FASTGlobal; describe("The FAST global", () => { context("kernel API", () => { diff --git a/packages/web-components/fast-element/src/platform.ts b/packages/web-components/fast-element/src/platform.ts index 83fbc6a45dc..38a0c5d06a2 100644 --- a/packages/web-components/fast-element/src/platform.ts +++ b/packages/web-components/fast-element/src/platform.ts @@ -1,117 +1,14 @@ -/** - * A policy for use with the standard trustedTypes platform API. - * @public - */ -export type TrustedTypesPolicy = { - /** - * Creates trusted HTML. - * @param html - The HTML to clear as trustworthy. - */ - createHTML(html: string): string; -}; - -/** - * Enables working with trusted types. - * @public - */ -export type TrustedTypes = { - /** - * Creates a trusted types policy. - * @param name - The policy name. - * @param rules - The policy rules implementation. - */ - createPolicy(name: string, rules: TrustedTypesPolicy): TrustedTypesPolicy; -}; - -/** - * The FAST global. - * @internal - */ -export interface FASTGlobal { - /** - * The list of loaded versions. - */ - readonly versions: string[]; - - /** - * Gets a kernel value. - * @param id - The id to get the value for. - * @param initialize - Creates the initial value for the id if not already existing. - */ - getById(id: string | number): T | null; - getById(id: string | number, initialize: () => T): T; -} - -/** - * The platform global type. - * @public - */ -export type Global = typeof globalThis & { - /** - * Enables working with trusted types. - */ - trustedTypes: TrustedTypes; - - /** - * The FAST global. - * @internal - */ - readonly FAST: FASTGlobal; -}; - -declare const global: any; - -/** - * A reference to globalThis, with support - * for browsers that don't yet support the spec. - * @public - */ -export const $global: Global = (function () { - if (typeof globalThis !== "undefined") { - // We're running in a modern environment. - return globalThis; - } - - if (typeof global !== "undefined") { - // We're running in NodeJS - return global; - } - - if (typeof self !== "undefined") { - // We're running in a worker. - return self; - } - - if (typeof window !== "undefined") { - // We're running in the browser's main thread. - return window; - } - - try { - // Hopefully we never get here... - // Not all environments allow eval and Function. Use only as a last resort: - // eslint-disable-next-line no-new-func - return new Function("return this")(); - } catch { - // If all fails, give up and create an object. - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return {}; - } -})(); - -// API-only Polyfill for trustedTypes -if (!$global.trustedTypes) { - $global.trustedTypes = { createPolicy: (n: string, r: TrustedTypesPolicy) => r }; -} +import type { FASTGlobal } from "./interfaces.js"; +// ensure FAST global - duplicated in polyfills.ts const propConfig = { configurable: false, enumerable: false, writable: false, }; -if ($global.FAST === void 0) { - Reflect.defineProperty($global, "FAST", { +if (globalThis.FAST === void 0) { + Reflect.defineProperty(globalThis, "FAST", { value: Object.create(null), ...propConfig, }); @@ -121,7 +18,7 @@ if ($global.FAST === void 0) { * The FAST global. * @internal */ -export const FAST = $global.FAST; +export const FAST: FASTGlobal = globalThis.FAST; if (FAST.getById === void 0) { const storage = Object.create(null); @@ -140,17 +37,6 @@ if (FAST.getById === void 0) { }); } -/** - * Core services shared across FAST instances. - * @internal - */ -export const enum KernelServiceId { - updateQueue = 1, - observable = 2, - contextEvent = 3, - elementRegistry = 4, -} - /** * A readonly, empty array. * @remarks diff --git a/packages/web-components/fast-element/src/polyfills.ts b/packages/web-components/fast-element/src/polyfills.ts new file mode 100644 index 00000000000..a53aa1e87ac --- /dev/null +++ b/packages/web-components/fast-element/src/polyfills.ts @@ -0,0 +1,121 @@ +import type { + FASTGlobal, + StyleStrategy, + StyleTarget, + TrustedTypesPolicy, +} from "./interfaces.js"; + +declare const global: any; + +(function ensureGlobalThis() { + if (typeof globalThis !== "undefined") { + // We're running in a modern environment. + return; + } + + if (typeof global !== "undefined") { + // We're running in NodeJS + global.globalThis = global; + } else if (typeof self !== "undefined") { + (self as any).globalThis = self; + } else if (typeof window !== "undefined") { + // We're running in the browser's main thread. + (window as any).globalThis = window; + } else { + // Hopefully we never get here... + // Not all environments allow eval and Function. Use only as a last resort: + // eslint-disable-next-line no-new-func + const result = new Function("return this")(); + result.globalThis = result; + } +})(); + +// API-only Polyfill for trustedTypes +if (!globalThis.trustedTypes) { + globalThis.trustedTypes = { + createPolicy: (n: string, r: TrustedTypesPolicy) => r, + }; +} + +// ensure FAST global - duplicated in platform.ts +const propConfig = { + configurable: false, + enumerable: false, + writable: false, +}; + +if (globalThis.FAST === void 0) { + Reflect.defineProperty(globalThis, "FAST", { + value: Object.create(null), + ...propConfig, + }); +} + +const FAST: FASTGlobal = globalThis.FAST; + +if (FAST.getById === void 0) { + const storage = Object.create(null); + + Reflect.defineProperty(FAST, "getById", { + value(id: string | number, initialize?: () => T): T | null { + let found = storage[id]; + + if (found === void 0) { + found = initialize ? (storage[id] = initialize()) : null; + } + + return found; + }, + ...propConfig, + }); +} + +// duplicated from DOM +const supportsAdoptedStyleSheets = + Array.isArray((document as any).adoptedStyleSheets) && + "replace" in CSSStyleSheet.prototype; + +function usableStyleTarget(target: StyleTarget): StyleTarget { + return target === document ? document.body : target; +} + +let id = 0; +const nextStyleId = (): string => `fast-${++id}`; + +export class StyleElementStrategy implements StyleStrategy { + private readonly styleClass: string; + + public constructor(private readonly styles: string[]) { + this.styleClass = nextStyleId(); + } + + public addStylesTo(target: StyleTarget): void { + target = usableStyleTarget(target); + + const styles = this.styles; + const styleClass = this.styleClass; + + for (let i = 0; i < styles.length; i++) { + const element = document.createElement("style"); + element.innerHTML = styles[i]; + element.className = styleClass; + target.append(element); + } + } + + public removeStylesFrom(target: StyleTarget): void { + const styles: NodeListOf = target.querySelectorAll( + `.${this.styleClass}` + ); + + target = usableStyleTarget(target); + + for (let i = 0, ii = styles.length; i < ii; ++i) { + target.removeChild(styles[i]); + } + } +} + +if (!supportsAdoptedStyleSheets) { + FAST.getById(/* KernelServiceId.styleSheetStrategy */ 5, () => StyleElementStrategy); +} diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 7bccb8377c2..a510001d666 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -1,35 +1,6 @@ import type { Behavior } from "../observation/behavior.js"; -import { DOM } from "../dom.js"; -import { nextId } from "../templating/markup.js"; - -/** - * A node that can be targeted by styles. - * @public - */ -export interface StyleTarget { - /** - * Stylesheets to be adopted by the node. - */ - adoptedStyleSheets?: CSSStyleSheet[]; - - /** - * Adds styles to the target by appending the styles. - * @param styles - The styles element to add. - */ - append(styles: HTMLStyleElement): void; - - /** - * Removes styles from the target. - * @param styles - The styles element to remove. - */ - removeChild(styles: HTMLStyleElement): void; - - /** - * Returns all element descendants of node that match selectors. - * @param selectors - The CSS selector to use for the query. - */ - querySelectorAll(selectors: string): NodeListOf; -} +import { FAST } from "../platform.js"; +import { KernelServiceId, StyleStrategy, StyleTarget } from "../interfaces.js"; /** * Represents styles that can be composed into the ShadowDOM of a custom element. @@ -37,25 +8,6 @@ export interface StyleTarget { */ export type ComposableStyles = string | ElementStyles | CSSStyleSheet; -/** - * Implemented to provide specific behavior when adding/removing styles - * for elements. - * @public - */ -export interface StyleStrategy { - /** - * Adds styles to the target. - * @param target - The target to add the styles to. - */ - addStylesTo(target: StyleTarget): void; - - /** - * Removes styles from the target. - * @param target - The target to remove the styles from. - */ - removeStylesFrom(target: StyleTarget): void; -} - /** * A type that instantiates a StyleStrategy. * @public @@ -210,47 +162,6 @@ export class AdoptedStyleSheetsStrategy implements StyleStrategy { } } -function usableTarget(target: StyleTarget): StyleTarget { - return target === document ? document.body : target; -} - -/** - * @internal - */ -export class StyleElementStrategy implements StyleStrategy { - private readonly styleClass: string; - - public constructor(private readonly styles: string[]) { - this.styleClass = nextId(); - } - - public addStylesTo(target: StyleTarget): void { - target = usableTarget(target); - - const styles = this.styles; - const styleClass = this.styleClass; - - for (let i = 0; i < styles.length; i++) { - const element = document.createElement("style"); - element.innerHTML = styles[i]; - element.className = styleClass; - target.append(element); - } - } - - public removeStylesFrom(target: StyleTarget): void { - const styles: NodeListOf = target.querySelectorAll( - `.${this.styleClass}` - ); - - target = usableTarget(target); - - for (let i = 0, ii = styles.length; i < ii; ++i) { - target.removeChild(styles[i]); - } - } -} - ElementStyles.setDefaultStrategy( - DOM.supportsAdoptedStyleSheets ? AdoptedStyleSheetsStrategy : StyleElementStrategy + FAST.getById(KernelServiceId.styleSheetStrategy, () => AdoptedStyleSheetsStrategy) ); diff --git a/packages/web-components/fast-element/src/styles/styles.spec.ts b/packages/web-components/fast-element/src/styles/styles.spec.ts index 19ba0c007bc..dfe946c4cc3 100644 --- a/packages/web-components/fast-element/src/styles/styles.spec.ts +++ b/packages/web-components/fast-element/src/styles/styles.spec.ts @@ -1,8 +1,6 @@ import { expect } from "chai"; import { AdoptedStyleSheetsStrategy, - StyleElementStrategy, - StyleTarget, ElementStyles, } from "./element-styles"; import { DOM } from "../dom"; @@ -11,6 +9,8 @@ import { css, cssPartial } from "./css"; import type { Behavior } from "../observation/behavior"; import { defaultExecutionContext } from "../observation/observable"; import type { FASTElement } from ".."; +import { StyleElementStrategy } from "../polyfills"; +import type { StyleTarget } from "../interfaces"; if (DOM.supportsAdoptedStyleSheets) { describe("AdoptedStyleSheetsStrategy", () => { diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 57cae98380a..8194b18ba00 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -176,9 +176,9 @@ function updateTokenListTarget( value: any ): void { const directive = this.directive; + const lookup = `${directive.uniqueId}-token-list`; const state: TokenListState = - target[directive.uniqueId] ?? - (target[directive.uniqueId] = { c: 0, v: Object.create(null) }); + target[lookup] ?? (target[lookup] = { c: 0, v: Object.create(null) }); const versions = state.v; let currentVersion = state.c; const tokenList = target[aspect] as DOMTokenList; @@ -493,6 +493,19 @@ export const signal = (options: string | Binding): BindingConfig return { mode: signalMode, options }; }; +declare class TrustedHTML {} +const createInnerHTMLBinding = globalThis.TrustedHTML + ? (binding: Binding) => (s, c) => { + const value = binding(s, c); + + if (value instanceof TrustedHTML) { + return value; + } + + throw new Error("To bind innerHTML, you must use a TrustedTypesPolicy."); + } + : (binding: Binding) => binding; + /** * @internal */ @@ -523,9 +536,7 @@ export class HTMLBindingDirective extends AspectedHTMLDirective { (this as Mutable).target = value.substring(1); switch (this.target) { case "innerHTML": - const binding = this.binding; - /* eslint-disable-next-line */ - this.binding = (s, c) => DOM.createHTML(binding(s, c)); + this.binding = createInnerHTMLBinding(this.binding); (this as Mutable).aspect = Aspect.property; break; case "classList": diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts index 3471a0a9724..e16ea181261 100644 --- a/packages/web-components/fast-element/src/templating/compiler.spec.ts +++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts @@ -4,12 +4,12 @@ import { customElement, FASTElement } from "../components/fast-element"; import { Markup } from './markup'; import { defaultExecutionContext } from "../observation/observable"; import { css } from "../styles/css"; -import type { StyleTarget } from "../styles/element-styles"; import { toHTML, uniqueElementName } from "../__test__/helpers"; import { bind, HTMLBindingDirective } from "./binding"; import { Compiler } from "./compiler"; import type { HTMLDirective, ViewBehaviorFactory } from "./html-directive"; import { html } from "./template"; +import type { StyleTarget } from "../interfaces"; /** * Used to satisfy TS by exposing some internal properties of the diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 53f60b724c6..baed861fd83 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,5 +1,4 @@ -import { isString } from "../interfaces.js"; -import { DOM } from "../dom.js"; +import { isString, TrustedTypesPolicy } from "../interfaces.js"; import type { ExecutionContext } from "../observation/observable.js"; import { Parser } from "./markup.js"; import { bind, oneTime } from "./binding.js"; @@ -226,8 +225,8 @@ function compileNode( case 8: // comment const parts = Parser.parse((node as Comment).data, context.directives); if (parts !== null) { - /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ context.addFactory( + /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ Compiler.aggregate(parts), parentId, nodeId, @@ -267,12 +266,31 @@ export type CompilationStrategy = ( ) => TemplateCompilationResult; const templateTag = "TEMPLATE"; +const policyOptions: TrustedTypesPolicy = { createHTML: html => html }; +let htmlPolicy: TrustedTypesPolicy = globalThis.trustedTypes + ? globalThis.trustedTypes.createPolicy("fast-html", policyOptions) + : policyOptions; +const fastHTMLPolicy = htmlPolicy; /** * Common APIs related to compilation. * @public */ export const Compiler = { + /** + * Sets the HTML trusted types policy used by the compiler. + * @param policy - The policy to set for HTML. + * @remarks + * This API can only be called once, for security reasons. It should be + * called by the application developer at the start of their program. + */ + setHTMLPolicy(policy: TrustedTypesPolicy) { + if (htmlPolicy !== fastHTMLPolicy) { + throw new Error("The HTML policy can only be set once."); + } + + htmlPolicy = policy; + }, /** * Compiles a template and associated directives into a compilation * result which can be used to create views. @@ -292,7 +310,7 @@ export const Compiler = { if (isString(html)) { template = document.createElement(templateTag) as HTMLTemplateElement; - template.innerHTML = DOM.createHTML(html); + template.innerHTML = htmlPolicy.createHTML(html); const fec = template.content.firstElementChild; diff --git a/packages/web-components/fast-element/src/templating/html-directive.ts b/packages/web-components/fast-element/src/templating/html-directive.ts index 0bb1a532fdd..4dc51a78025 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -89,6 +89,7 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { /** * The type of HTML aspect to target. + * @public */ export enum Aspect { /** @@ -156,7 +157,10 @@ export abstract class AspectedHTMLDirective extends HTMLDirective { public createPlaceholder: (index: number) => string = Markup.interpolation; } -/** @internal */ +/** + * A base class used for attribute directives that don't need internal state. + * @public + */ export abstract class StatelessAttachedAttributeDirective extends HTMLDirective implements ViewBehavior { /** diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.template.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.template.ts index 26b6d5d5825..cb12277240e 100644 --- a/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.template.ts +++ b/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.template.ts @@ -14,9 +14,8 @@ export const dataGridCellTemplate: FoundationElementTemplate`