├── .yarnrc.yml ├── .prettierrc ├── src ├── options │ ├── style.css │ ├── index.html │ └── options-script.ts ├── env.d.ts ├── manifest.json ├── types.ts ├── injected.ts ├── options.ts ├── plugins.ts └── background.ts ├── .gitignore ├── .vscode └── settings.json ├── parcel-transformer-replace ├── package.json └── index.js ├── .parcelrc ├── tsconfig.json ├── .github └── workflows │ └── build-extension.yaml ├── .yarn └── patches │ └── @parcel-transformer-webextension-npm-2.8.2-0a4803c81c.patch └── package.json /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.3.1.cjs 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "printWidth": 140 4 | } 5 | -------------------------------------------------------------------------------- /src/options/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px; 3 | } 4 | 5 | section { 6 | width: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | var browser: import("webextension-polyfill").Browser; 2 | function __prettierTextArea(parser: import("./plugins").AvailableParser): void; 3 | var PRETTIER_DEBUG: boolean | undefined; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache 2 | web-ext-artifacts 3 | yarn-error.log 4 | dist 5 | node_modules 6 | test.html 7 | 8 | .yarn/* 9 | .yarn/cache 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.env.windows": { 3 | "WEB_EXT_FIREFOX": "C:\\Program Files\\WindowsApps\\Mozilla.Firefox_108.0.1.0_x64__n80bbvh6b1yt2\\VFS\\ProgramFiles\\Firefox Package Root\\firefox.exe" 4 | } 5 | } -------------------------------------------------------------------------------- /parcel-transformer-replace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-transformer-replace", 3 | "main": "index.js", 4 | "engines": { 5 | "parcel": "^2.8.2" 6 | }, 7 | "dependencies": { 8 | "@parcel/plugin": "2.8.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-webextension", 3 | "transformers": { 4 | "*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ 5 | "parcel-transformer-replace", 6 | "@parcel/transformer-babel", 7 | "@parcel/transformer-js", 8 | "@parcel/transformer-react-refresh-wrap" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "lib": [ 5 | "DOM" 6 | ], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } -------------------------------------------------------------------------------- /parcel-transformer-replace/index.js: -------------------------------------------------------------------------------- 1 | exports.default = new (require("@parcel/plugin").Transformer)({ 2 | async transform({ asset }) { 3 | if (asset.filePath.includes("node_modules")) { 4 | // remove an eval for `globalThis` from some dependencies - those would trigger the DANGEROUS_EVAL web-ext lint rule 5 | asset.setCode((await asset.getCode()).replace(/Function\(['"]return this['"]\)\(\)/, "globalThis")); 6 | } 7 | 8 | return [asset]; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.github/workflows/build-extension.yaml: -------------------------------------------------------------------------------- 1 | name: build extension 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-node@v3 9 | with: 10 | node-version: 16 11 | cache: "yarn" 12 | - run: yarn install --frozen-lockfile 13 | - run: yarn build 14 | - run: yarn lint 15 | - uses: actions/upload-artifact@v3 16 | with: 17 | name: extension 18 | path: | 19 | web-ext-artifacts 20 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/webextension.json", 3 | "manifest_version": 2, 4 | "name": "Format with Prettier", 5 | "description": "Use prettier from your browser!", 6 | "version": "0.0.1", 7 | "background": { 8 | "scripts": ["../node_modules/webextension-polyfill/dist/browser-polyfill.js", "background.ts"] 9 | }, 10 | "options_ui": { 11 | "page": "options/index.html", 12 | "browser_style": true 13 | }, 14 | "permissions": ["activeTab", "contextMenus", "storage"], 15 | "browser_specific_settings": { 16 | "gecko": { 17 | "id": "format-with-prettier@phryneas.de" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "prettier"; 2 | import { AvailableParser } from "./plugins"; 3 | 4 | declare module "prettier" { 5 | export interface Options { 6 | /** 7 | * undocumented feature: 8 | * this would allow someone to specify additional parsers to be loaded without having to have them in a context menu 9 | * (e.g. if you only want to have one `Markdown` option, but want all the parsers to be loaded) 10 | */ 11 | extraParsers?: AvailableParser[]; 12 | } 13 | } 14 | 15 | export interface FormatRequest { 16 | code: string; 17 | options: Partial; 18 | unparsedOptions?: string; 19 | } 20 | export interface FormatResponse { 21 | formatted: string; 22 | error?: string; 23 | } 24 | -------------------------------------------------------------------------------- /.yarn/patches/@parcel-transformer-webextension-npm-2.8.2-0a4803c81c.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/WebExtensionTransformer.js b/lib/WebExtensionTransformer.js 2 | index eebb41184db7ebac344ee745698daf45b93bdd25..f78332c85f21bb72b46928e5c6d995c21305f67a 100644 3 | --- a/lib/WebExtensionTransformer.js 4 | +++ b/lib/WebExtensionTransformer.js 5 | @@ -411,8 +411,8 @@ var _default = new (_plugin().Transformer)({ 6 | browsers: asset.env.engines.browsers 7 | }, 8 | sourceMap: asset.env.sourceMap && { ...asset.env.sourceMap, 9 | - inline: true, 10 | - inlineSources: true 11 | + inline: false, 12 | + inlineSources: false 13 | }, 14 | includeNodeModules: asset.env.includeNodeModules, 15 | sourceType: asset.env.sourceType, 16 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Format with Prettier

9 |
10 |

Prettier Options

11 | 12 | 13 | 14 |
15 |
16 |

Parsers

17 |
18 |
19 |

Other

20 |
21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/injected.ts: -------------------------------------------------------------------------------- 1 | import type { Options as FormatOptions } from "prettier"; 2 | import { getOption } from "./options"; 3 | import { AvailableParser } from "./plugins"; 4 | import { FormatRequest, FormatResponse } from "./types"; 5 | 6 | async function __prettierTextArea(parser: AvailableParser) { 7 | try { 8 | const element = [document.activeElement, document.querySelector(":focus")].find( 9 | (element): element is HTMLInputElement | HTMLTextAreaElement => 10 | !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) 11 | ); 12 | 13 | if (!element) return; 14 | 15 | const options: FormatOptions = { 16 | parser, 17 | }; 18 | if (element.selectionStart != null && element.selectionEnd != null && element.selectionStart != element.selectionEnd) { 19 | options.rangeStart = element.selectionStart; 20 | options.rangeEnd = element.selectionEnd; 21 | } 22 | const request: FormatRequest = { 23 | code: element.value, 24 | options, 25 | }; 26 | const response: FormatResponse = await browser.runtime.sendMessage(request); 27 | if (response.error) throw new Error(response.error); 28 | element.value = response.formatted; 29 | } catch (e) { 30 | const msg = `formatting with parser ${parser} failed: 31 | ${e?.toString()}`; 32 | console.log(await getOption("alertOnFormatError")); 33 | if (await getOption("alertOnFormatError")) { 34 | alert(msg); 35 | } 36 | console.error(msg); 37 | } 38 | } 39 | 40 | window.__prettierTextArea = __prettierTextArea; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "format-with-prettier", 3 | "version": "0.0.1", 4 | "author": "Lenz Weber ", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@parcel/config-webextension": "^2.8.2", 8 | "@types/prettier": "^2.7.2", 9 | "@types/webextension-polyfill": "^0.9.2", 10 | "assert": "^2.0.0", 11 | "buffer": "^5.5.0", 12 | "crypto-browserify": "^3.12.0", 13 | "os-browserify": "^0.3.0", 14 | "parcel": "^2.8.2", 15 | "path-browserify": "^1.0.0", 16 | "stream-browserify": "^3.0.0", 17 | "tty-browserify": "^0.0.1", 18 | "typescript": "^4.9.4", 19 | "util": "^0.12.3", 20 | "web-ext": "^7.4.0", 21 | "webextension-polyfill": "^0.10.0" 22 | }, 23 | "scripts": { 24 | "watch": "npm run clean && npm run parcel:start", 25 | "build": "npm run clean && npm run parcel:build && npm run ext:build", 26 | "lint": "npm run ext:lint", 27 | "firefox": "npm run ext:run", 28 | "clean": "rimraf dist", 29 | "parcel:start": "parcel watch src/manifest.json --host localhost", 30 | "parcel:build": "parcel build src/manifest.json", 31 | "ext:run": "web-ext run -s ./dist --devtools", 32 | "ext:build": "web-ext build -s ./dist", 33 | "ext:lint": "web-ext lint -s ./dist" 34 | }, 35 | "dependencies": { 36 | "@babel/parser": "^7.20.7", 37 | "parcel-transformer-replace": "workspace:^", 38 | "prettier": "^2.8.1", 39 | "remark-parse": "^10.0.1", 40 | "json5": "2.2.2" 41 | }, 42 | "packageManager": "yarn@3.3.1", 43 | "workspaces": [ 44 | "./parcel-transformer-replace" 45 | ], 46 | "resolutions": { 47 | "@parcel/transformer-webextension": "patch:@parcel/transformer-webextension@npm%3A2.8.2#./.yarn/patches/@parcel-transformer-webextension-npm-2.8.2-0a4803c81c.patch" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { AvailableParser } from "./plugins"; 2 | import { storage } from "webextension-polyfill"; 3 | 4 | const defaultPrettierOptions = `{ 5 | // see https://prettier.io/docs/en/options.html 6 | printWidth: 80, 7 | useTabs: false, 8 | tabWidth: 2, 9 | semi: true, 10 | singleQuote: false, 11 | quoteProps: "as-needed", 12 | trailingComma: "es5", 13 | bracketSpacing: true, 14 | bracketSameLine: false, 15 | arrowParens: "always", 16 | proseWrap: "preserve", 17 | htmlWhitespaceSensitivity: "css", 18 | vueIndentScriptAndStyle: false, 19 | endOfLine: "auto", 20 | embeddedLanguageFormatting: "auto", 21 | singleAttributePerLine: false, 22 | /** 23 | * Here you can specify parsers of plugins that should be loaded, 24 | * even if they are not checked down below. 25 | * E.g. if you only want the "Markdown" context menu, 26 | * but still want js blocks to be formatted, you can set: 27 | * extraParsers: [ 'babel' ] 28 | */ 29 | extraParsers: [], 30 | } 31 | `; 32 | 33 | export const defaultOptions = { 34 | enabledParsers: ["babel", "typescript", "markdown", "html", "json5"] as AvailableParser[], 35 | prettierOptions: defaultPrettierOptions, 36 | alertOnFormatError: true, 37 | rethrowEmbedErrors: true, 38 | }; 39 | export type ExtensionOptions = typeof defaultOptions; 40 | 41 | export async function getOptions() { 42 | return storage.sync.get(defaultOptions) as Promise; 43 | } 44 | 45 | export async function getOption(option: K): Promise { 46 | return (await getOptions())[option]; 47 | } 48 | 49 | export function setOption(option: K, value: ExtensionOptions[K]) { 50 | return storage.sync.set({ [option]: value }); 51 | } 52 | 53 | export function onOptionChange(option: K, handler: () => void) { 54 | storage.sync.onChanged.addListener((changes) => { 55 | if (option in changes) { 56 | handler(); 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/plugins.ts: -------------------------------------------------------------------------------- 1 | import { BuiltInParserName, Plugin as PrettierPlugin } from "prettier"; 2 | 3 | export interface PluginDescription { 4 | plugin: () => Promise; 5 | name: string; 6 | types: Partial>; 7 | } 8 | 9 | export const plugins = [ 10 | { 11 | plugin: () => import("prettier/parser-babel"), 12 | name: "Babel", 13 | types: { 14 | babel: "JavaScript", 15 | "babel-ts": "TypeScript", 16 | "babel-flow": "Flow", 17 | json: "JSON", 18 | json5: "JSON5", 19 | "json-stringify": "json-stringify", 20 | }, 21 | }, 22 | { 23 | plugin: () => import("prettier/parser-html"), 24 | name: "HTML", 25 | types: { html: "HTML", vue: "Vue" }, 26 | }, 27 | { 28 | plugin: () => import("prettier/parser-graphql"), 29 | name: "GraphQL", 30 | types: { graphql: "GraphQL" }, 31 | }, 32 | { 33 | plugin: () => import("prettier/parser-markdown"), 34 | name: "Markdown", 35 | types: { 36 | markdown: "Markdown", 37 | mdx: "MDX", 38 | }, 39 | }, 40 | { 41 | plugin: () => import("prettier/parser-postcss"), 42 | name: "PostCSS", 43 | types: { 44 | css: "CSS", 45 | less: "Less", 46 | scss: "Sass", 47 | }, 48 | }, 49 | { 50 | plugin: () => import("prettier/parser-typescript"), 51 | name: "TypeScript", 52 | types: { typescript: "TypeScript" }, 53 | }, 54 | { 55 | plugin: () => import("prettier/parser-yaml"), 56 | name: "YAML", 57 | types: { yaml: "YAML" }, 58 | }, 59 | // other plugins currently not included: 60 | // acorn: "JavaScript (Acorn)", 61 | // angular: "Angular", 62 | // espree: "JavaScript (Espree)", 63 | // flow: "Flow", 64 | // glimmer: "Handlebars", 65 | // lwc: "HTML (Lwc)", 66 | // meriyah: "JavaScript (Meriyah)", 67 | ] as const satisfies readonly PluginDescription[]; 68 | 69 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; 70 | 71 | type PluginDefs = typeof plugins; 72 | export type AvailableParser = keyof UnionToIntersection; 73 | 74 | export const parsersByName = Object.fromEntries( 75 | plugins.flatMap(({ name, plugin, types }) => 76 | Object.entries(types).map( 77 | ([parser, description]) => 78 | [ 79 | parser, 80 | { 81 | pluginName: name, 82 | plugin, 83 | parser: parser as AvailableParser, 84 | description, 85 | }, 86 | ] as const 87 | ) 88 | ) 89 | ); 90 | 91 | export function getEnabledParsersWithName(enabledParsers: AvailableParser[]) { 92 | return Object.values(parsersByName).filter((p) => enabledParsers.includes(p.parser)); 93 | } 94 | 95 | export async function getEnabledPluginsByParserName(enabledParsers: AvailableParser[]): Promise { 96 | const pluginLoaders = new Set<() => Promise>(); 97 | for (const parser of enabledParsers) { 98 | pluginLoaders.add(parsersByName[parser].plugin); 99 | } 100 | return Promise.all([...pluginLoaders.values()].map((fn) => fn())); 101 | } 102 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import type { Options as FormatOptions } from "prettier"; 2 | import type { Tabs } from "webextension-polyfill"; 3 | import { getOption, onOptionChange } from "./options"; 4 | import { AvailableParser, getEnabledParsersWithName, getEnabledPluginsByParserName } from "./plugins"; 5 | import type { FormatRequest, FormatResponse } from "./types"; 6 | import { parse as parseJson } from "json5"; 7 | 8 | const prettier = import("prettier/standalone"); 9 | 10 | (async function () { 11 | try { 12 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 13 | try { 14 | if (!tab || typeof info.menuItemId != "string") return; 15 | const match = /^prettier-textarea-(.*)$/.exec(info.menuItemId); 16 | if (match) { 17 | const parser = match[1]; 18 | 19 | await ensureScriptLoaded(tab); 20 | await browser.tabs.executeScript(tab.id, { 21 | code: `__prettierTextArea(${JSON.stringify(parser)})`, 22 | }); 23 | } 24 | } catch (e) { 25 | console.error("an error occured", e?.toString()); 26 | } 27 | }); 28 | 29 | browser.runtime.onMessage.addListener(async (request: FormatRequest): Promise => { 30 | try { 31 | const format = (await prettier).format; 32 | const { extraParsers = [], ...requestOptions } = request.options; 33 | 34 | const options: FormatOptions = { 35 | ...parseJson(await getOption("prettierOptions")), 36 | ...requestOptions, 37 | ...parseJson(request.unparsedOptions ?? "{}"), 38 | plugins: await getEnabledPluginsByParserName([ 39 | ...extraParsers, 40 | ...(await getOption("enabledParsers")), 41 | requestOptions.parser as AvailableParser, 42 | ]), 43 | }; 44 | if (await getOption("rethrowEmbedErrors")) { 45 | globalThis.PRETTIER_DEBUG = true; 46 | } 47 | return { 48 | formatted: format(request.code, options), 49 | }; 50 | } catch (e) { 51 | return { 52 | formatted: request.code, 53 | error: `Error while formatting: 54 | ${e?.toString()}`, 55 | }; 56 | } finally { 57 | globalThis.PRETTIER_DEBUG = undefined; 58 | } 59 | }); 60 | 61 | onOptionChange("enabledParsers", refreshContextMenu); 62 | await refreshContextMenu(); 63 | } catch (e) { 64 | console.error("background page error", e?.toString()); 65 | } 66 | })(); 67 | 68 | async function refreshContextMenu() { 69 | const activeParsers = getEnabledParsersWithName(await getOption("enabledParsers")); 70 | const onlyOne = activeParsers.length == 1; 71 | browser.contextMenus.removeAll(); 72 | for (const { parser, pluginName, description } of activeParsers) { 73 | const descriptionUnique = activeParsers.filter((p) => p.description == description).length == 1; 74 | const parserDescription = descriptionUnique ? description : `${description} (${pluginName})`; 75 | const title = onlyOne ? `Format with Prettier (${parserDescription})` : parserDescription; 76 | browser.contextMenus.create({ 77 | id: `prettier-textarea-${parser}`, 78 | title, 79 | contexts: ["editable"], 80 | }); 81 | } 82 | } 83 | 84 | async function ensureScriptLoaded(currentTab: Tabs.Tab) { 85 | const results = await browser.tabs.executeScript(currentTab.id, { 86 | code: "typeof __prettierTextArea === 'function';", 87 | }); 88 | 89 | if (!results || results[0] !== true) { 90 | const file = new URL("injected.ts", import.meta.url).toString(); 91 | return browser.tabs.executeScript(currentTab.id, { file }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/options/options-script.ts: -------------------------------------------------------------------------------- 1 | import { defaultOptions, getOption, getOptions, ExtensionOptions, setOption } from "../options"; 2 | import { parsersByName } from "../plugins"; 3 | import { FormatRequest, FormatResponse } from "../types"; 4 | 5 | init().catch((e) => { 6 | console.error("error in options page", e.toString()); 7 | }); 8 | 9 | async function init() { 10 | const $checkboxTemplate = document.getElementById("checkbox-template")! as HTMLTemplateElement; 11 | const $parsers = document.getElementById("parsers")!; 12 | const $other = document.getElementById("other")!; 13 | const $prettierOptions: HTMLTextAreaElement = document.querySelector('[name="prettier-options"]')!; 14 | const $prettierOptionsReset = document.getElementById("prettier-options-reset")!; 15 | const $prettierOptionsErrors = document.getElementById("prettier-options-errors")!; 16 | 17 | $prettierOptionsReset.onclick = () => { 18 | if (confirm("do you really want to reset the prettier options to the default value?")) { 19 | $prettierOptions.value = defaultOptions.prettierOptions; 20 | $prettierOptions.onblur?.(new FocusEvent("blur")); 21 | } 22 | }; 23 | 24 | const options = await getOptions(); 25 | 26 | function resizeOptionsTextarea() { 27 | $prettierOptions.rows = $prettierOptions.value.split("\n").length + 1; 28 | } 29 | $prettierOptions.onkeydown = resizeOptionsTextarea; 30 | $prettierOptions.value = options.prettierOptions; 31 | resizeOptionsTextarea(); 32 | $prettierOptions.onblur = async () => { 33 | try { 34 | const value = $prettierOptions.value; 35 | const request: FormatRequest = { 36 | code: $prettierOptions.value, 37 | options: { parser: "json5" }, 38 | unparsedOptions: value, 39 | }; 40 | const response: FormatResponse = await browser.runtime.sendMessage(request); 41 | await setOption("prettierOptions", response.formatted); 42 | if (response.error) { 43 | alert(response.error); 44 | } else { 45 | $prettierOptions.value = response.formatted; 46 | resizeOptionsTextarea(); 47 | } 48 | 49 | $prettierOptionsErrors.innerText = ""; 50 | } catch (e) { 51 | $prettierOptionsErrors.innerText = `Error while saving settings: 52 | ${e?.toString()}`; 53 | } 54 | }; 55 | 56 | addCheckbox({ 57 | label: "alert on error", 58 | checked: options.alertOnFormatError, 59 | onChange(newValue) { 60 | setOption("alertOnFormatError", newValue); 61 | }, 62 | parent: $other, 63 | }); 64 | addCheckbox({ 65 | label: "rethrow ebmed errors", 66 | checked: options.rethrowEmbedErrors, 67 | onChange(newValue) { 68 | setOption("rethrowEmbedErrors", newValue); 69 | }, 70 | parent: $other, 71 | }); 72 | 73 | for (const parser of Object.values(parsersByName)) { 74 | addCheckbox({ 75 | label: `${parser.parser}: ${parser.description} (${parser.pluginName})`, 76 | async onChange(newValue) { 77 | const oldParsers = await getOption("enabledParsers"); 78 | await setOption("enabledParsers", newValue ? oldParsers.concat(parser.parser) : oldParsers.filter((p) => p != parser.parser)); 79 | }, 80 | checked: options.enabledParsers.includes(parser.parser), 81 | parent: $parsers, 82 | }); 83 | } 84 | 85 | function addCheckbox({ 86 | label, 87 | onChange, 88 | checked, 89 | parent, 90 | }: { 91 | label: string; 92 | onChange: (newValue: boolean) => void; 93 | checked: boolean; 94 | parent: HTMLElement; 95 | }) { 96 | const $elem = document.importNode($checkboxTemplate.content, true); 97 | 98 | const $label = $elem.querySelector(".description")!; 99 | const $input: HTMLInputElement = $elem.querySelector("input")!; 100 | 101 | $label.textContent = label; 102 | $input.checked = checked; 103 | $input.onchange = () => onChange($input.checked); 104 | parent.append($elem); 105 | } 106 | } 107 | --------------------------------------------------------------------------------