From 8a0f687f6e3c320a7a9b4d82fc77a252cc7e500e Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Wed, 10 Dec 2025 23:48:10 +0800 Subject: [PATCH] Refactor playground to use Vue (#18463) Co-authored-by: fisker <172584+fisker@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/renovate.json5 | 5 - eslint.config.js | 13 +- package.json | 1 - website/index.html | 12 - website/package.json | 9 +- website/playground/EditorState.js | 58 ++-- website/playground/Playground.jsx | 312 ++++++++--------- website/playground/PrettierFormat.js | 86 +++-- website/playground/VersionLink.jsx | 30 +- website/playground/buttons.jsx | 82 +++-- website/playground/helpers.js | 49 --- website/playground/index.jsx | 163 ++++----- website/playground/panels.jsx | 229 +++++++----- website/playground/sidebar/SidebarOptions.jsx | 18 +- website/playground/sidebar/components.jsx | 12 +- website/playground/sidebar/inputs.jsx | 42 ++- website/playground/sidebar/options.jsx | 39 ++- website/static/service-worker.mjs | 2 - website/vite.config.mjs | 7 +- website/yarn.lock | 327 +++++++++++++----- yarn.lock | 269 +------------- 21 files changed, 851 insertions(+), 914 deletions(-) delete mode 100644 website/playground/helpers.js diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 2d4ae8f47..781717cc7 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -33,7 +33,6 @@ matchPackageNames: [ "eslint", "@eslint/*", - "@eslint-react/eslint-plugin", "@stylistic/eslint-plugin", "eslint-config-prettier", "eslint-plugin-compat", @@ -75,10 +74,6 @@ groupName: "GitHub Actions", matchManagers: ["github-actions"], }, - { - matchPackageNames: ["react", "react-dom"], - allowedVersions: "^18.0.0", - }, { groupName: "docusaurus", matchPackageNames: ["@docusaurus/*"], diff --git a/eslint.config.js b/eslint.config.js index b32e79a90..8b3f29049 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,5 @@ import url from "node:url"; import eslintPluginJs from "@eslint/js"; -import eslintPluginEslintReact from "@eslint-react/eslint-plugin"; import eslintPluginStylistic from "@stylistic/eslint-plugin"; import eslintPluginTypescriptEslint from "@typescript-eslint/eslint-plugin"; import { isCI } from "ci-info"; @@ -457,8 +456,11 @@ const configs = [ }, }, { - files: ["website/**/*"], - ...eslintPluginEslintReact.configs.recommended, + files: ["website/src/pages/**/*.jsx", "website/playground/**/*.jsx"], + rules: { + // Doesn't know JSX + "no-unused-vars": "off", + }, }, { files: ["website/**/*"], @@ -470,11 +472,6 @@ const configs = [ }, }, }, - settings: { - "react-x": { - version: "18", - }, - }, rules: { "unicorn/filename-case": "off", }, diff --git a/package.json b/package.json index f030bf952..e42e4cb90 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,6 @@ }, "devDependencies": { "@babel/generator": "8.0.0-beta.3", - "@eslint-react/eslint-plugin": "2.3.9", "@eslint/js": "9.39.1", "@stylistic/eslint-plugin": "5.6.1", "@types/jsesc": "3.0.3", diff --git a/website/index.html b/website/index.html index 9d038506d..ffd4ba9e4 100644 --- a/website/index.html +++ b/website/index.html @@ -156,18 +156,6 @@ crossorigin="anonymous" referrerpolicy="no-referrer" > - - diff --git a/website/package.json b/website/package.json index 5807f05a0..91b7b8cf4 100644 --- a/website/package.json +++ b/website/package.json @@ -21,19 +21,22 @@ "@sandhose/prettier-animated-logo": "1.0.3", "clsx": "2.1.1", "codemirror-graphql": "2.2.4", + "graphql": "16.12.0", "prism-react-renderer": "2.4.1", "react": "18.3.1", "react-dom": "18.3.1", "react-markdown": "10.1.0", - "react-tweet": "3.2.2" + "react-tweet": "3.2.2", + "vue": "3.5.25" }, "devDependencies": { "@docusaurus/faster": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/types": "3.9.2", - "@vitejs/plugin-react": "5.1.1", + "@vitejs/plugin-vue": "6.0.2", + "@vitejs/plugin-vue-jsx": "5.1.2", "concurrently": "9.2.1", - "vite": "7.2.4", + "vite": "7.2.7", "yaml": "2.8.2" }, "browserslist": { diff --git a/website/playground/EditorState.js b/website/playground/EditorState.js index 0b5876598..21c620dce 100644 --- a/website/playground/EditorState.js +++ b/website/playground/EditorState.js @@ -1,12 +1,13 @@ -import { shallowEqual, stateToggler } from "./helpers.js"; +import { reactive, watch } from "vue"; import * as storage from "./storage.js"; -const { React } = window; - -export default class EditorState extends React.Component { - constructor() { - super(); - this.state = { +export default { + name: "EditorState", + setup(_, { slots }) { + const stateToggler = (property) => () => { + state[property] = !state[property]; + }; + const state = reactive({ showSidebar: window.innerWidth > window.innerHeight, showAst: false, showPreprocessedAst: false, @@ -16,28 +17,27 @@ export default class EditorState extends React.Component { showInput: true, showOutput: true, rethrowEmbedErrors: false, - toggleSidebar: () => this.setState(stateToggler("showSidebar")), - toggleAst: () => this.setState(stateToggler("showAst")), - togglePreprocessedAst: () => - this.setState(stateToggler("showPreprocessedAst")), - toggleDoc: () => this.setState(stateToggler("showDoc")), - toggleComments: () => this.setState(stateToggler("showComments")), - toggleSecondFormat: () => this.setState(stateToggler("showSecondFormat")), - toggleInput: () => this.setState(stateToggler("showInput")), - toggleOutput: () => this.setState(stateToggler("showOutput")), - toggleEmbedErrors: () => - this.setState(stateToggler("rethrowEmbedErrors")), + toggleSidebar: stateToggler("showSidebar"), + toggleAst: stateToggler("showAst"), + togglePreprocessedAst: stateToggler("showPreprocessedAst"), + toggleDoc: stateToggler("showDoc"), + toggleComments: stateToggler("showComments"), + toggleSecondFormat: stateToggler("showSecondFormat"), + toggleInput: stateToggler("showInput"), + toggleOutput: stateToggler("showOutput"), + toggleEmbedErrors: stateToggler("rethrowEmbedErrors"), ...storage.get("editor_state"), - }; - } + }); - componentDidUpdate(_, prevState) { - if (!shallowEqual(this.state, prevState)) { - storage.set("editor_state", this.state); - } - } + const render = () => slots.default(state); - render() { - return this.props.children(this.state); - } -} + watch( + () => state, + () => { + storage.set("editor_state", state); + }, + { deep: true }, + ); + return render; + }, +}; diff --git a/website/playground/Playground.jsx b/website/playground/Playground.jsx index 190bfa101..91ad47206 100644 --- a/website/playground/Playground.jsx +++ b/website/playground/Playground.jsx @@ -1,8 +1,8 @@ +import { reactive, watch } from "vue"; import { Button, ClipboardButton } from "./buttons.jsx"; import getCodeSample from "./codeSamples.mjs"; import generateDummyId from "./dummyId.js"; import EditorState from "./EditorState.js"; -import { shallowEqual } from "./helpers.js"; import formatMarkdown from "./markdown.js"; import { DebugPanel, InputPanel, OutputPanel } from "./panels.jsx"; import PrettierFormat from "./PrettierFormat.js"; @@ -20,8 +20,6 @@ import { getDefaults, } from "./utilities.js"; -const { React } = window; - const CATEGORIES_ORDER = [ "Global", "Common", @@ -63,124 +61,108 @@ const ENABLED_OPTIONS = [ "experimentalOperatorPosition", ]; -class Playground extends React.Component { - constructor(props) { - super(); +function setup(props) { + const original = urlHash.read(); - const original = urlHash.read(); + const defaultOptions = getDefaults(props.availableOptions, ENABLED_OPTIONS); - const defaultOptions = getDefaults(props.availableOptions, ENABLED_OPTIONS); + const options = Object.assign(defaultOptions, original.options); - const options = Object.assign(defaultOptions, original.options); + // 0.0.0 ~ 0.0.10 + if (options.parser === "babylon") { + options.parser = "babel"; + } - // 0.0.0 ~ 0.0.10 - if (options.parser === "babylon") { - options.parser = "babel"; - } + // 0.0.0 ~ 0.0.10 + if (options.useFlowParser) { + options.parser ??= "flow"; + } - // 0.0.0 ~ 0.0.10 - if (options.useFlowParser) { - options.parser ??= "flow"; - } + // 1.8.2 ~ 1.9.0 + if (typeof options.proseWrap === "boolean") { + options.proseWrap = options.proseWrap ? "always" : "never"; + } - // 1.8.2 ~ 1.9.0 - if (typeof options.proseWrap === "boolean") { - options.proseWrap = options.proseWrap ? "always" : "never"; - } + // 0.0.0 ~ 1.9.0 + if (typeof options.trailingComma === "boolean") { + options.trailingComma = options.trailingComma ? "es5" : "none"; + } - // 0.0.0 ~ 1.9.0 - if (typeof options.trailingComma === "boolean") { - options.trailingComma = options.trailingComma ? "es5" : "none"; - } + // 0.17.0 ~ 2.4.0 + if (options.jsxBracketSameLine) { + delete options.jsxBracketSameLine; + options.bracketSameLine ??= options.jsxBracketSameLine; + } - // 0.17.0 ~ 2.4.0 - if (options.jsxBracketSameLine) { - delete options.jsxBracketSameLine; - options.bracketSameLine ??= options.jsxBracketSameLine; - } + const codeSample = getCodeSample(options.parser); + const content = original.content || codeSample; - const codeSample = getCodeSample(options.parser); - const content = original.content || codeSample; - const selection = {}; + const state = reactive({ content, options, selection: {} }); - this.state = { content, options, selection }; + const setContent = (content) => { + state.content = content; + }; - this.handleOptionValueChange = this.handleOptionValueChange.bind(this); + const clearContent = () => { + state.content = ""; + }; - this.setContent = (content) => this.setState({ content }); - this.clearContent = this.setContent.bind(this, ""); - this.resetOptions = () => this.setState({ options: defaultOptions }); - this.setSelection = (selection) => { - this.setState({ selection }); - if (this.state.trackCursorOffset) { - this.handleOptionValueChange( - this.cursorOffsetOption, - convertSelectionToRange(selection, this.state.content)[0], - ); - } - }; - this.setSelectionAsRange = () => { - const { selection, content, options } = this.state; - const [rangeStart, rangeEnd] = convertSelectionToRange( - selection, - content, + const resetOptions = () => { + Object.assign(state, { options: { ...defaultOptions } }); + }; + + const setSelection = (selection) => { + Object.assign(state, { selection }); + if (state.trackCursorOffset) { + handleOptionValueChange( + cursorOffsetOption, + convertSelectionToRange(selection, state.content)[0], ); - const updatedOptions = { ...options, rangeStart, rangeEnd }; - if (rangeStart === rangeEnd) { - delete updatedOptions.rangeStart; - delete updatedOptions.rangeEnd; - } - this.setState({ options: updatedOptions }); - }; - - this.enabledOptions = orderOptions(props.availableOptions, ENABLED_OPTIONS); - this.rangeStartOption = props.availableOptions.find( - (opt) => opt.name === "rangeStart", - ); - this.rangeEndOption = props.availableOptions.find( - (opt) => opt.name === "rangeEnd", - ); - this.cursorOffsetOption = props.availableOptions.find( - (opt) => opt.name === "cursorOffset", - ); - - this.formatInput = this.formatInput.bind(this); - this.insertDummyId = this.insertDummyId.bind(this); - } - - componentDidUpdate(_, prevState) { - const { content, options } = this.state; - if ( - !shallowEqual(prevState.options, this.state.options) || - prevState.content !== content - ) { - urlHash.replace({ content, options }); } - } + }; - handleOptionValueChange(option, value) { - this.setState((state) => { - const options = { ...state.options }; + const setSelectionAsRange = () => { + const { selection, content, options } = state; + const [rangeStart, rangeEnd] = convertSelectionToRange(selection, content); + const updatedOptions = { ...options, rangeStart, rangeEnd }; + if (rangeStart === rangeEnd) { + delete updatedOptions.rangeStart; + delete updatedOptions.rangeEnd; + } + Object.assign(state, { options: updatedOptions }); + }; + const enabledOptions = orderOptions(props.availableOptions, ENABLED_OPTIONS); + const rangeStartOption = props.availableOptions.find( + (opt) => opt.name === "rangeStart", + ); + const rangeEndOption = props.availableOptions.find( + (opt) => opt.name === "rangeEnd", + ); + const cursorOffsetOption = props.availableOptions.find( + (opt) => opt.name === "cursorOffset", + ); - if (option.type === "int" && Number.isNaN(value)) { - delete options[option.name]; - } else { - options[option.name] = value; - } + const handleOptionValueChange = (option, value) => { + const options = { ...state.options }; - const content = - state.content === "" || - state.content === getCodeSample(state.options.parser) - ? getCodeSample(options.parser) - : state.content; + if (option.type === "int" && Number.isNaN(value)) { + delete options[option.name]; + } else { + options[option.name] = value; + } - return { options, content }; - }); - } + const content = + state.content === "" || + state.content === getCodeSample(state.options.parser) + ? getCodeSample(options.parser) + : state.content; - getMarkdown({ formatted, reformatted, full, doc }) { - const { content, options } = this.state; - const { availableOptions, version } = this.props; + return Object.assign(state, { options, content }); + }; + + const getMarkdown = ({ formatted, reformatted, full, doc }) => { + const { content, options } = state; + const { availableOptions, version } = props; const orderedOptions = orderOptions(availableOptions, [ ...ENABLED_OPTIONS, "rangeStart", @@ -200,16 +182,16 @@ class Playground extends React.Component { cliOptions, full, }); - } + }; - formatInput() { - if (this.state.options.parser !== "doc-explorer") { + const formatInput = () => { + if (state.options.parser !== "doc-explorer") { return; } - const { content, selection } = this.state; + const { content, selection } = state; - return this.props.worker + return props.worker .format(content, { parser: "__js_expression", cursorOffset: convertSelectionToRange(selection, content)[0], @@ -219,32 +201,32 @@ class Playground extends React.Component { return; } - this.setState({ + Object.assign(state, { content: formatted, selection: convertOffsetToSelection(cursorOffset, formatted), }); }); - } + }; - insertDummyId() { - const { content, selection } = this.state; + const insertDummyId = () => { + const { content, selection } = state; const dummyId = generateDummyId(); const range = convertSelectionToRange(selection, content); const modifiedContent = content.slice(0, range[0]) + dummyId + content.slice(range[1]); - this.setState({ + Object.assign(state, { content: modifiedContent, selection: convertOffsetToSelection( range[0] + dummyId.length, modifiedContent, ), }); - } + }; - render() { - const { worker } = this.props; - const { content, options } = this.state; + const render = () => { + const { worker } = props; + const { content, options } = state; return ( @@ -261,7 +243,8 @@ class Playground extends React.Component { rethrowEmbedErrors={editorState.rethrowEmbedErrors} > {({ formatted, debug, cursorOffset }) => { - const fullReport = this.getMarkdown({ + const { content, options } = state; + const fullReport = getMarkdown({ formatted, reformatted: debug.reformatted, full: true, @@ -272,13 +255,13 @@ class Playground extends React.Component { return ( <> -
+
{isDocExplorer ? null : ( @@ -287,26 +270,26 @@ class Playground extends React.Component { the input editor @@ -314,13 +297,13 @@ class Playground extends React.Component { {isDocExplorer ? null : ( -
+
{editorState.showInput ? ( @@ -484,12 +466,12 @@ class Playground extends React.Component { ) : null}
-
-
+
+
- +
-
+
Copy link - this.getMarkdown({ + getMarkdown({ formatted, reformatted: debug.reformatted, }) @@ -548,8 +530,26 @@ class Playground extends React.Component { )} ); - } + }; + + watch( + () => [state.content, state.options], + () => { + urlHash.replace({ content: state.content, options: state.options }); + }, + { deep: true }, + ); + return render; } +const Playground = { + name: "Playground", + props: { + worker: { type: Object, required: true }, + availableOptions: { type: Array, required: true }, + version: { type: String, required: true }, + }, + setup, +}; function orderOptions(availableOptions, order) { const optionsByName = {}; @@ -569,7 +569,7 @@ function getSecondFormat(formatted, reformatted) { ? "" : formatted === reformatted ? "✓ Second format is unchanged." - : reformatted; + : (reformatted ?? ""); } export default Playground; diff --git a/website/playground/PrettierFormat.js b/website/playground/PrettierFormat.js index 668206f9e..6268e4c83 100644 --- a/website/playground/PrettierFormat.js +++ b/website/playground/PrettierFormat.js @@ -1,36 +1,14 @@ -const { React } = window; +import { onMounted, reactive, toRaw, watch } from "vue"; -export default class PrettierFormat extends React.Component { - constructor() { - super(); - this.state = { formatted: "", debug: {} }; - } +function setup(props, { slots }) { + const state = reactive({ formatted: "", debug: {} }); - componentDidMount() { - this.format(); - } + const componentDidMount = () => { + format(); + }; - componentDidUpdate(prevProps) { - for (const key of [ - "enabled", - "code", - "options", - "debugAst", - "debugPreprocessedAst", - "debugDoc", - "debugComments", - "reformat", - "rethrowEmbedErrors", - ]) { - if (prevProps[key] !== this.props[key]) { - this.format(); - break; - } - } - } - - async format() { - const { + const format = async () => { + let { worker, code, options, @@ -40,7 +18,8 @@ export default class PrettierFormat extends React.Component { debugComments: comments, reformat, rethrowEmbedErrors, - } = this.props; + } = props; + options = toRaw(options); const result = await worker.format(code, options, { ast, @@ -51,10 +30,45 @@ export default class PrettierFormat extends React.Component { rethrowEmbedErrors, }); - this.setState(result); - } + Object.assign(state, result); + }; - render() { - return this.props.children(this.state); - } + const render = () => slots.default(state); + + onMounted(componentDidMount); + watch( + () => + [ + "enabled", + "code", + "options", + "debugAst", + "debugPreprocessedAst", + "debugDoc", + "debugComments", + "reformat", + "rethrowEmbedErrors", + ].map((property) => props[property]), + () => { + format(); + }, + { deep: true }, + ); + return render; } + +export default { + name: "PrettierFormat", + props: { + worker: Object, + code: String, + options: Object, + debugAst: Boolean, + debugPreprocessedAst: Boolean, + debugDoc: Boolean, + debugComments: Boolean, + reformat: Boolean, + rethrowEmbedErrors: Boolean, + }, + setup, +}; diff --git a/website/playground/VersionLink.jsx b/website/playground/VersionLink.jsx index c5a23e09f..e96cfad0a 100644 --- a/website/playground/VersionLink.jsx +++ b/website/playground/VersionLink.jsx @@ -1,6 +1,4 @@ -const { React, ReactDOM } = window; - -const root = document.getElementById("version"); +import { Teleport } from "vue"; export default function VersionLink({ version }) { const match = version.match(/^pr-(\d+)$/u); @@ -15,18 +13,20 @@ export default function VersionLink({ version }) { const formattedVersion = match ? `PR #${match[1]}` : `v${version}`; - React.useEffect(() => { - document.title = `Prettier ${formattedVersion}`; - }, [formattedVersion]); + document.title = `Prettier ${formattedVersion}`; - return ReactDOM.createPortal( - - {formattedVersion} - , - root, + return ( + + + {formattedVersion} + + ); } +VersionLink.props = { + version: { type: String, required: true }, +}; diff --git a/website/playground/buttons.jsx b/website/playground/buttons.jsx index 1227e54d3..f8e0e0284 100644 --- a/website/playground/buttons.jsx +++ b/website/playground/buttons.jsx @@ -1,49 +1,61 @@ -const { ClipboardJS, React } = window; +import { onMounted, reactive, ref } from "vue"; -export const Button = React.forwardRef((props, ref) => ( - +); - componentDidMount() { - this.clipboard = new ClipboardJS(this.ref.current, { - text: () => { - const { copy } = this.props; +function setup(props, { slots, attrs }) { + const state = reactive({ showTooltip: false, tooltipText: "" }); + let timer = null; + const buttonRef = ref(); + + const componentDidMount = () => { + const clipboard = new ClipboardJS(buttonRef.value, { + text() { + const { copy } = props; return typeof copy === "function" ? copy() : copy; }, }); - this.clipboard.on("success", () => this.showTooltip("Copied!")); - this.clipboard.on("error", () => this.showTooltip("Press ctrl+c to copy")); - } + clipboard.on("success", () => showTooltip("Copied!")); + clipboard.on("error", () => showTooltip("Press ctrl+c to copy")); + }; - showTooltip(text) { - this.setState({ showTooltip: true, tooltipText: text }, () => { - if (this.timer) { - clearTimeout(this.timer); - } - this.timer = setTimeout(() => { - this.timer = null; - this.setState({ showTooltip: false }); - }, 2000); - }); - } + const showTooltip = (text) => { + Object.assign(state, { showTooltip: true, tooltipText: text }); - render() { - const { children, copy, ...rest } = this.props; - const { showTooltip, tooltipText } = this.state; + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = null; + Object.assign(state, { showTooltip: false }); + }, 2000); + }; + + const render = () => { + const { showTooltip, tooltipText } = state; + const children = slots.default(); return ( - ); - } + }; + + onMounted(componentDidMount); + return render; } + +export const ClipboardButton = { + name: "ClipboardButton", + props: { + copy: { type: [String, Function], required: true }, + }, + setup, +}; diff --git a/website/playground/helpers.js b/website/playground/helpers.js deleted file mode 100644 index 36363cd17..000000000 --- a/website/playground/helpers.js +++ /dev/null @@ -1,49 +0,0 @@ -export function stateToggler(key) { - return (state) => ({ [key]: !state[key] }); -} - -const { hasOwnProperty } = Object.prototype; - -function is(x, y) { - // SameValue algorithm - if (x === y) { - // Steps 1-5, 7-10 - // Steps 6.b-6.e: +0 != -0 - return x !== 0 || 1 / x === 1 / y; - } - // Step 6.a: NaN == NaN - return x !== x && y !== y; -} - -export function shallowEqual(objA, objB) { - if (is(objA, objB)) { - return true; - } - - if ( - typeof objA !== "object" || - objA === null || - typeof objB !== "object" || - objB === null - ) { - return false; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - for (let i = 0; i < keysA.length; i++) { - if ( - !hasOwnProperty.call(objB, keysA[i]) || - !is(objA[keysA[i]], objB[keysA[i]]) - ) { - return false; - } - } - - return true; -} diff --git a/website/playground/index.jsx b/website/playground/index.jsx index f26d4b325..da7e1de58 100644 --- a/website/playground/index.jsx +++ b/website/playground/index.jsx @@ -1,16 +1,12 @@ import "codemirror-graphql/cm6-legacy/mode.esm.js"; import "./install-service-worker.js"; +import { createApp, onMounted, reactive, ref, watch } from "vue"; import Playground from "./Playground.jsx"; import { fixPrettierVersion } from "./utilities.js"; import VersionLink from "./VersionLink.jsx"; import WorkerApi from "./WorkerApi.js"; -const { - React, - ReactDOM: { createRoot }, -} = window; - function getInitialTheme() { const saved = localStorage.getItem("theme"); if (saved) { @@ -27,92 +23,97 @@ function applyTheme(theme) { } function ThemeToggle() { - const [theme, setTheme] = React.useState(getInitialTheme); + const theme = ref(getInitialTheme()); const toggleTheme = () => { - const newTheme = theme === "dark" ? "light" : "dark"; - setTheme(newTheme); + const newTheme = theme.value === "dark" ? "light" : "dark"; + theme.value = newTheme; localStorage.setItem("theme", newTheme); applyTheme(newTheme); }; - applyTheme(theme); - return React.createElement( - "button", - { - onClick: toggleTheme, - className: "btn", - "aria-label": `Switch to ${theme === "dark" ? "light" : "dark"} theme`, - }, - theme === "dark" - ? React.createElement( - "svg", - { - class: "theme-icon", - viewBox: "0 0 24 24", - width: "14", - height: "14", - "aria-hidden": "true", - }, - React.createElement("path", { - fill: "currentColor", - d: "M9.37 5.51a7.4 7.4 0 009.12 9.12A7 7 0 119.37 5.51M12 3a9 9 0 108.9 7.64 5.39 5.39 0 01-9.8-3.14 5.4 5.4 0 012.26-4.4Q12.69 3 12 3", - }), - ) - : React.createElement( - "svg", - { - class: "theme-icon", - viewBox: "0 0 24 24", - width: "14", - height: "14", - "aria-hidden": "true", - }, - React.createElement("path", { - fill: "currentColor", - d: "M12 9a1 1 0 0 1 0 6 1 1 0 0 1 0-6m0-2a5 5 0 1 0 0 10 5 5 0 0 0 0-10M2 13h2a1 1 0 0 0 0-2H2a1 1 0 0 0 0 2m18 0h2a1 1 0 0 0 0-2h-2a1 1 0 0 0 0 2M11 2v2a1 1 0 0 0 2 0V2a1 1 0 0 0-2 0m0 18v2a1 1 0 0 0 2 0v-2a1 1 0 0 0-2 0M5.99 4.58a1 1 0 1 0-1.41 1.41l1.06 1.06a1 1 0 1 0 1.41-1.41zm12.37 12.37a1 1 0 1 0-1.41 1.41l1.06 1.06a1 1 0 1 0 1.41-1.41zm1.06-10.96a1 1 0 1 0-1.41-1.41l-1.06 1.06a1 1 0 1 0 1.41 1.41zM7.05 18.36a1 1 0 1 0-1.41-1.41l-1.06 1.06a1 1 0 1 0 1.41 1.41z", - }), - ), + applyTheme(theme.value); + + watch(theme, (newTheme) => { + applyTheme(newTheme); + }); + + return ( + ); } -class App extends React.Component { - constructor() { - super(); - this.state = { loaded: false }; - this.worker = new WorkerApi(); - } +const App = { + name: "App", + setup() { + const state = reactive({ loaded: false }); + const worker = new WorkerApi(); - async componentDidMount() { - const { supportInfo, version } = await this.worker.getMetadata(); + const componentDidMount = async () => { + const { supportInfo, version } = await worker.getMetadata(); - // eslint-disable-next-line @eslint-react/no-set-state-in-component-did-mount - this.setState({ - loaded: true, - availableOptions: supportInfo.options.map(augmentOption), - version: fixPrettierVersion(version), - }); - } + Object.assign(state, { + loaded: true, + availableOptions: supportInfo.options.map(augmentOption), + version: fixPrettierVersion(version), + }); + }; - render() { - const { loaded, availableOptions, version } = this.state; + const render = () => { + const { loaded, availableOptions, version } = state; - if (!loaded) { - return "Loading..."; - } + if (!loaded) { + return "Loading..."; + } - return ( - <> - - - - ); - } -} + return ( + <> + + + + ); + }; + + onMounted(componentDidMount); + return render; + }, +}; function augmentOption(option) { if (option.type === "boolean" && option.default === true) { @@ -131,10 +132,10 @@ const linksContainer = document.querySelector(".links"); if (linksContainer) { const themeToggleContainer = document.createElement("div"); linksContainer.appendChild(themeToggleContainer); - const themeToggleRoot = createRoot(themeToggleContainer); - themeToggleRoot.render(); + const themeToggleRoot = createApp(ThemeToggle); + themeToggleRoot.mount(themeToggleContainer); } const container = document.getElementById("root"); -const root = createRoot(container); -root.render(); +const root = createApp(App); +root.mount(container); diff --git a/website/playground/panels.jsx b/website/playground/panels.jsx index 8d993db12..461c557b6 100644 --- a/website/playground/panels.jsx +++ b/website/playground/panels.jsx @@ -1,141 +1,180 @@ -const { React, CodeMirror } = window; +import { onMounted, onUnmounted, useTemplateRef, watch } from "vue"; -class CodeMirrorPanel extends React.Component { - constructor() { - super(); - this._textareaRef = React.createRef(); - this._codeMirror = null; - this._cached = ""; - this._overlay = null; - this.handleChange = this.handleChange.bind(this); - this.handleFocus = this.handleFocus.bind(this); - this.handleSelectionChange = this.handleSelectionChange.bind(this); - } +const { CodeMirror } = window; - componentDidMount() { - const options = { ...this.props }; - delete options.ruler; - delete options.rulerColor; - delete options.value; - delete options.selection; - delete options.onChange; +function setup(props, { emit }) { + const textareaRef = useTemplateRef("textarea"); + let _codeMirror = null; + let _cached = ""; + let _overlay = null; + const onChange = (value) => { + emit("change", value); + }; - options.rulers = [makeRuler(this.props)]; - options.gutters = makeGutters(this.props); + const componentDidMount = () => { + const options = { ...props }; + for (const property of [ + "ruler", + "rulerColor", + "value", + "selection", + "onChange", + ]) { + delete options[property]; + } - this._codeMirror = CodeMirror.fromTextArea( - this._textareaRef.current, - options, - ); - this._codeMirror.on("change", this.handleChange); - this._codeMirror.on("focus", this.handleFocus); - this._codeMirror.on("beforeSelectionChange", this.handleSelectionChange); + for (const [key, value] of Object.entries(options)) { + if (value === undefined) { + delete options[key]; + } + } + + options.rulers = [makeRuler(props)]; + options.gutters = makeGutters(props); + + options.rulers = [makeRuler(props)]; + options.gutters = makeGutters(props); + + _codeMirror = CodeMirror.fromTextArea(textareaRef.value, options); + _codeMirror.on("change", handleChange); + _codeMirror.on("focus", handleFocus); + _codeMirror.on("beforeSelectionChange", handleSelectionChange); window.CodeMirror.keyMap.pcSublime["Ctrl-L"] = false; window.CodeMirror.keyMap.sublime["Ctrl-L"] = false; - this.updateValue(this.props.value || ""); - this.updateSelection(); - this.updateOverlay(); - } + updateValue(props.value || ""); + updateSelection(); + updateOverlay(); + }; - componentWillUnmount() { - this._codeMirror && this._codeMirror.toTextArea(); - } + const componentWillUnmount = () => { + _codeMirror?.toTextArea(); + }; - componentDidUpdate(prevProps) { - if (this.props.value !== this._cached) { - this.updateValue(this.props.value); + const componentDidUpdate = (_, prevProps) => { + if (!_codeMirror) { + return; + } + + if (props.value !== _cached) { + updateValue(props.value); } if ( - !isEqualSelection(this.props.selection, prevProps.selection) && - !isEqualSelection( - this.props.selection, - this._codeMirror.listSelections()[0], - ) + !isEqualSelection(props.selection, prevProps.selection) && + !isEqualSelection(props.selection, _codeMirror.listSelections()[0]) ) { - this.updateSelection(); + updateSelection(); } if ( - this.props.overlayStart !== prevProps.overlayStart || - this.props.overlayEnd !== prevProps.overlayEnd + props.overlayStart !== prevProps.overlayStart || + props.overlayEnd !== prevProps.overlayEnd ) { - this.updateOverlay(); + updateOverlay(); } - if (this.props.mode !== prevProps.mode) { - this._codeMirror.setOption("mode", this.props.mode); + if (props.mode !== prevProps.mode) { + _codeMirror.setOption("mode", props.mode); } - if (this.props.ruler !== prevProps.ruler) { - this._codeMirror.setOption("rulers", [makeRuler(this.props)]); + if (props.ruler !== prevProps.ruler) { + _codeMirror.setOption("rulers", [makeRuler(props)]); } - if (this.props.foldGutter !== prevProps.foldGutter) { - this._codeMirror.setOption("gutters", makeGutters(this.props)); + if (props.foldGutter !== prevProps.foldGutter) { + _codeMirror.setOption("gutters", makeGutters(props)); } - } + }; - updateValue(value) { - this._cached = value; - this._codeMirror.setValue(value); + const updateValue = (value) => { + _cached = value; + _codeMirror.setValue(value); - if (this.props.autoFold instanceof RegExp) { + if (props.autoFold instanceof RegExp) { const lines = value.split("\n"); // going backwards to prevent unfolding folds created earlier for (let i = lines.length - 1; i >= 0; i--) { - if (this.props.autoFold.test(lines[i])) { - this._codeMirror.foldCode(i); + if (props.autoFold.test(lines[i])) { + _codeMirror.foldCode(i); } } } - } + }; - updateSelection() { - this._codeMirror.setSelection( - this.props.selection?.anchor ?? { line: 0, ch: 0 }, - this.props.selection?.head, + const updateSelection = () => { + _codeMirror.setSelection( + props.selection?.anchor ?? { line: 0, ch: 0 }, + props.selection?.head, ); - } + }; - updateOverlay() { - if (this._overlay) { - this._codeMirror.removeOverlay(this._overlay); + const updateOverlay = () => { + if (_overlay) { + _codeMirror.removeOverlay(_overlay); } - if (this.props.overlayStart !== undefined) { - const [start, end] = getIndexPosition(this.props.value, [ - this.props.overlayStart, - this.props.overlayEnd, + if (props.overlayStart !== undefined) { + const [start, end] = getIndexPosition(props.value, [ + props.overlayStart, + props.overlayEnd, ]); - this._overlay = createOverlay(start, end); - this._codeMirror.addOverlay(this._overlay); + _overlay = createOverlay(start, end); + _codeMirror.addOverlay(_overlay); } - } + }; - handleFocus(/* codeMirror, event */) { - if (this._codeMirror.getValue() === this.props.codeSample) { - this._codeMirror.execCommand("selectAll"); + const handleFocus = (/* codeMirror, event */) => { + if (_codeMirror.getValue() === props.codeSample) { + _codeMirror.execCommand("selectAll"); } - } + }; - handleChange(doc, change) { + const handleChange = (doc, change) => { if (change.origin !== "setValue") { - this._cached = doc.getValue(); - this.props.onChange(this._cached); - this.updateOverlay(); + _cached = doc.getValue(); + onChange(_cached); + updateOverlay(); } - } + }; - handleSelectionChange(doc, change) { - this.props.onSelectionChange?.(change.ranges[0]); - } + const handleSelectionChange = (doc, change) => { + props.onSelectionChange?.(change.ranges[0]); + }; - render() { - return ( -
-