From 3f9cf020a0d620e46299d24defe0851b353b7b18 Mon Sep 17 00:00:00 2001 From: timongh <46739861+timongh@users.noreply.github.com> Date: Sun, 2 Oct 2022 03:07:43 +0800 Subject: [PATCH] Perfect the type of switch options --- .../components/style/simplify/home/index.ts | 93 +++-- .../components/style/simplify/live/index.ts | 4 +- src/components/switch-options.ts | 361 +++++++++++++++--- 3 files changed, 364 insertions(+), 94 deletions(-) diff --git a/registry/lib/components/style/simplify/home/index.ts b/registry/lib/components/style/simplify/home/index.ts index 1b8ce162d..90957df50 100644 --- a/registry/lib/components/style/simplify/home/index.ts +++ b/registry/lib/components/style/simplify/home/index.ts @@ -1,12 +1,16 @@ -import { createSwitchOptions, SwitchOptions } from '@/components/switch-options' -import { ComponentMetadata } from '@/components/types' +import { + newSwitchComponentBuilder, + defineSwitchMetadata, + defineIncompleteSwitchComponentMetadata, +} from '@/components/switch-options' + import { addComponentListener, getComponentSettings } from '@/core/settings' import { sq } from '@/core/spin-query' import { addStyle } from '@/core/style' import { getCookieValue } from '@/core/utils' import { mainSiteUrls } from '@/core/utils/urls' -const switchOptions: SwitchOptions = { +const switchMetadata = defineSwitchMetadata({ name: 'simplifyOptions', dimAt: 'checked', switchProps: { @@ -43,8 +47,9 @@ const switchOptions: SwitchOptions = { displayName: '右侧分区导航(旧)', }, }, -} -const metadata: ComponentMetadata = { +}) + +const metadata = defineIncompleteSwitchComponentMetadata({ name: 'simplifyHome', displayName: '简化首页', description: { @@ -81,13 +86,17 @@ const metadata: ComponentMetadata = { () => dqa('.proxy-box > div'), elements => elements.length > 0 || isNotHome, ) - return Object.fromEntries(categoryElements.map(it => ([ - it.id.replace(/^bili_/, ''), - { - displayName: it.querySelector('header .name')?.textContent?.trim() ?? '未知分区', - defaultValue: false, - }, - ]))) + return Object.fromEntries( + categoryElements.map(it => [ + it.id.replace(/^bili_/, ''), + { + displayName: + it.querySelector('header .name')?.textContent?.trim() + ?? '未知分区', + defaultValue: false, + }, + ]), + ) } const skipIds = ['推广'] @@ -123,39 +132,47 @@ const metadata: ComponentMetadata = { } return null }) - .filter((it): it is [string, SimplifyHomeOption] => it !== null) ?? [] + .filter((it): it is [string, SimplifyHomeOption] => it !== null) + ?? [] return Object.fromEntries(entries) })() const generatedSwitches: Record = {} - Object.entries(generatedOptions).forEach(([key, { displayName, defaultValue }]) => { - const option = { - defaultValue, - displayName, - } - const optionKey = `switch-${key}` - if (options[optionKey] === undefined) { - options[optionKey] = defaultValue - } - const switchKey = `switch-${key}` - addComponentListener( - `${metadata.name}.${switchKey}`, - (value: boolean) => { - document.body.classList.toggle(`${metadata.name}-${switchKey}`, value) - }, - true, - ) - switchOptions.switches[key] = option - generatedSwitches[key] = option - }) - options.simplifyOptions.switches = generatedSwitches - const generatedStyles = Object.keys(generatedOptions).map(name => ` + Object.entries(generatedOptions).forEach( + ([key, { displayName, defaultValue }]) => { + const option = { + defaultValue, + displayName, + } + const optionKey = `switch-${key}` + if (options[optionKey] === undefined) { + options[optionKey] = defaultValue + } + const switchKey = `switch-${key}` + addComponentListener( + `${metadata.name}.${switchKey}`, + (value: boolean) => { + document.body.classList.toggle( + `${metadata.name}-${switchKey}`, + value, + ) + }, + true, + ) + switchMetadata.switches[key] = option + generatedSwitches[key] = option + }, + ); + (options.simplifyOptions as any).switches = generatedSwitches + const generatedStyles = Object.keys(generatedOptions) + .map(name => ` body.simplifyHome-switch-${name} .bili-layout .bili-grid[data-area="${name}"], body.simplifyHome-switch-${name} .storey-box .proxy-box #bili_${name} { display: none !important; } - `.trim()).join('\n') + `.trim()) + .join('\n') addStyle(generatedStyles, 'simplify-home-generated') }, -} +}) -export const component = createSwitchOptions(switchOptions)(metadata) +export const component = newSwitchComponentBuilder(switchMetadata)(metadata) diff --git a/registry/lib/components/style/simplify/live/index.ts b/registry/lib/components/style/simplify/live/index.ts index 684f4cea5..b0d869534 100644 --- a/registry/lib/components/style/simplify/live/index.ts +++ b/registry/lib/components/style/simplify/live/index.ts @@ -1,8 +1,8 @@ -import { createSwitchOptions } from '@/components/switch-options' +import { newSwitchComponentBuilder } from '@/components/switch-options' import { styledComponentEntry } from '@/components/styled-component' import { liveUrls } from '@/core/utils/urls' -export const component = createSwitchOptions({ +export const component = newSwitchComponentBuilder({ name: 'simplifyOptions', dimAt: 'checked', switchProps: { diff --git a/src/components/switch-options.ts b/src/components/switch-options.ts index 26d251aa2..1006987d5 100644 --- a/src/components/switch-options.ts +++ b/src/components/switch-options.ts @@ -1,71 +1,324 @@ -import { getComponentSettings, addComponentListener } from '@/core/settings' -import { ComponentMetadata, ComponentOptions } from './component' +/** + * Switch Options + * @module components/switch-options + * + * 通过包装原始的 ComponentMetadata 为组件提供一系列开关选项。 + * 如果组件未定义 Widget,还会提供一个默认的有相同效果的 Widget。 + * + * 主要函数是 {@link newSwitchComponentBuilder}。 + */ -type Switches = { - [key: string]: { - displayName: string - defaultValue: boolean - } +import { getComponentSettings, addComponentListener } from '@/core/settings' +import { + ComponentEntry, + ComponentMetadata, + OptionsMetadata, + OptionsOfMetadata, + UnknownOptions, +} from './component' +import { Widget } from './widget' + +/** + * 单个开关的设置 + */ +export interface SwitchItemMetadata { + displayName: string + defaultValue: boolean } -export interface SwitchOptions { - name: string - switches: Switches - radio?: boolean - dimAt?: 'checked' | 'notChecked' + +/** + * 一个对象,键是开关的名称,用于生成 options;值是关于单个开关的设置 + */ +export type SwitchItemsMetadata = { + [key in S]: SwitchItemMetadata +} + +/** + * 从 SwitchItemsMetadata 中提取所有 SwitchItem 的名称 + */ +export type SwitchItemNamesOfItemsMetadata> = keyof C + +/** + * 可用于单独定义 SwitchItemsMetadata + */ +export const defineSwitchItemsMetadata = ( + c: SwitchItemsMetadata, +): SwitchItemsMetadata => c + +/** + * 用于配置 API 的行为 + * + * {@link SwitchItemsMetadata} 可用 {@link defineSwitchItemsMetadata} 单独定义 + */ +export interface SwitchMetadata { + // 注入 options 时会使用 + name: N + // 每个开关的单独配置,其键名会被用于生成注入 options 的名称,见 {@link SwitchItemNames} + switches: SwitchItemsMetadata + // 是否单选 default: false + radio?: undefined | boolean + // default: 'checked' + dimAt?: undefined | 'checked' | 'notChecked' + // 配置每个开关的图标 switchProps?: { checkedIcon?: string notCheckedIcon?: string iconPosition?: 'left' | 'right' } } -export const createSwitchOptions = (options: SwitchOptions) => { - if (options.radio === undefined) { - options.radio = false + +/** + * 从 SwitchMetadata 中提取 name 的字面量类型(如果可能) + */ +export type SwitchNameOfMetadata> = C['name'] + +/** + * 从 SwitchMetadata 中提取所有 SwitchItem 的名称 + */ +export type SwitchItemNamesOfMetadata> = + keyof C['switches'] + +/** + * 定义一个 {@link SwitchMetadata} + */ +export const defineSwitchMetadata = ( + c: SwitchMetadata, +): SwitchMetadata => c + +/** + * 传递给 Widget 的选项。同时也是注入组件的 options 的一部分 + */ +export interface SwitchWidgetOptions { + name: N + switches: SwitchItemsMetadata + radio: boolean + dimAt: 'checked' | 'notChecked' + switchProps?: { + checkedIcon?: string + notCheckedIcon?: string + iconPosition?: 'left' | 'right' } - const { name: optionName, switches } = options - const extendComponentOptions: ComponentOptions = {} - Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => { - extendComponentOptions[`switch-${key}`] = { + componentName: string + optionDisplayName: string +} + +/** + * 创建一个 SwitchWidgetOptions + */ +const newSwitchWidgetOptions = ( + metadata: SwitchMetadata, + componentName: string, + optionDisplayName, +) => ({ + ...metadata, + radio: metadata.radio === undefined ? false : metadata.radio, + dimAt: metadata.dimAt === undefined ? 'checked' : metadata.dimAt, + componentName, + optionDisplayName, + }) + +/** + * 注入的 options 的名称集合。规则是 SwitchItemsMetadata 的所有键加上 'switch-' 前缀。 + */ +export type SwitchItemNames = `switch-${S}` + +/** + * API 注入的 options 中的一部分 + */ +export type SwitchItemOptions = Record, string> + +/** + * 在原始的 Options 类型中添加了该 API 所需的定义后形成的类型 + */ +export type SwitchOptions = O & { + N: SwitchWidgetOptions +} & SwitchItemOptions + +/** + * 提取 SwitchOptions 类型 + */ +export type SwitchOptionsOfSwitchMetadata< + O extends UnknownOptions, + C extends SwitchMetadata, +> = C extends SwitchMetadata ? SwitchOptions : never + +/** + * 提取 SwitchOptions 类型 + */ +export type SwitchOptionsOfMetadata< + M extends OptionsMetadata, + C extends SwitchMetadata, +> = SwitchOptionsOfSwitchMetadata, C> + +/** + * 在原始的 OptionsMetadata 中添加了该 API 所需的定义后形成的类型 + */ +export type SwitchOptionsMetadata< + O extends UnknownOptions, + N extends string, + S extends string, +> = OptionsMetadata> + +/** + * 创建一个 extender。 + * + * extender 是一个函数,其作用是为传入的 OptionsMetadata 添加新字段。 + * + * 该函数会直接修改传入的 OptionsMetadata,并将其返回。 + */ +const newSwitchOptionsMetadataExtender = ( + itemsMetadata: SwitchItemsMetadata, +): (( + options: OptionsMetadata, + widgetOptions: SwitchWidgetOptions, +) => SwitchOptionsMetadata) => { + const optionsToExtend = {} + const entries = Object.entries(itemsMetadata) + for (const [key, { displayName, defaultValue }] of entries) { + optionsToExtend[`switch-${key}`] = { defaultValue, displayName, hidden: true, } - }) - return (component: ComponentMetadata) => { - const optionDisplayName = `${component.displayName}选项` - const selfOption = { - componentName: component.name, - optionDisplayName, - } - Object.assign(options, selfOption) - extendComponentOptions[optionName] = { + } + return ( + options: OptionsMetadata, + widgetOptions: SwitchWidgetOptions, + ) => { + optionsToExtend[widgetOptions.name as string] = { defaultValue: options, - displayName: optionDisplayName, + displayName: widgetOptions.optionDisplayName, } - component.options = { ...component.options, ...extendComponentOptions } - if (!component.widget) { - component.widget = { - component: () => import('./SwitchOptions.vue').then(m => m.default), - options, - } - } - const originalEntry = component.entry - component.entry = async (...args) => { - originalEntry?.(...args) - const { name } = component - const componentOptions = getComponentSettings(name).options - Object.keys(componentOptions).forEach(key => { - if (key.startsWith('switch-')) { - addComponentListener( - `${name}.${key}`, - (value: boolean) => { - document.body.classList.toggle(`${name}-${key}`, value) - }, - true, - ) - } - }) - } - return component + Object.assign(options, optionsToExtend) + return options as SwitchOptionsMetadata + } +} + +/** + * 创建默认 Widget + */ +const newWidget = ( + options: SwitchWidgetOptions, +): Omit => ({ + component: () => import('./SwitchOptions.vue').then(m => m.default), + options, + }) + +/** + * 包装后的 entry 函数,函数参数类型中包含 API 注入的 `options` + */ +export type SwitchEntry< + O extends UnknownOptions, + N extends string, + S extends string, +> = ComponentEntry> + +/** + * 类似于 `ComponentMetadata`。 + * + * 定义该类型时,请使用 {@link defineIncompleteSwitchComponentMetadata} + * + * 不同之处在于,该类型在定义时,可在此 API 的相关 `options` 注入前, + * 让 `entry` 函数获取到完整类型提示。 + */ +export type IncompleteSwitchComponentMetadata< + O extends UnknownOptions, + N extends string, + S extends string, +> = { + [K in keyof ComponentMetadata]: K extends 'entry' + ? SwitchEntry + : ComponentMetadata[K] +} + +/** + * 辅助定义一个 {@link IncompleteSwitchComponentMetadata}, + * 类似 {@link ./define.ts defineComponentMetadata}。 + */ +export const defineIncompleteSwitchComponentMetadata = < + O extends UnknownOptions, + N extends string, + S extends string, +>( + m: IncompleteSwitchComponentMetadata, + ): IncompleteSwitchComponentMetadata => m + +/** + * 包装原始 entry 函数并返回 + */ +const newSwitchEntry = ( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, +): SwitchEntry => (...args) => { + const result = component.entry(...args) + const componentOptions = getComponentSettings(component.name).options + Object.keys(componentOptions).forEach(key => { + if (key.startsWith('switch-')) { + addComponentListener( + `${component.name}.${key}`, + (value: boolean) => { + document.body.classList.toggle(`${component.name}-${key}`, value) + }, + true, + ) + } + }) + return result + } + +/** + * 被包装后的 `ComponentMetadata` + */ +export type SwitchComponentMetadata< + O extends UnknownOptions, + N extends string, + S extends string, +> = ComponentMetadata> + +/** + * 创建一个 builder + * + * builder 是一个函数,该函数接收一个 `ComponentMetadata`,为其添加 `options`。 + * 如果组件为未定义 Widget,还会提供一个默认的有相同效果的 Widget。 + * + * 定义 {@link SwitchMetadata} 请使用 {@link defineSwitchMetadata} + * + * 所注入的 options 参考 {@link SwitchOptions}。 + * Widget 的选项见 {@link SwitchWidgetOptions}。 + * + * 如果想在定义原始 `ComponentMetadata` 时提前获得被注入的 `options` 的类型提示, + * 可以使用 {@link defineIncompleteSwitchComponentMetadata} 函数辅助定义 + * + * 为了实现效果,builder 还会对 `entry` 进行包装, + * 因此包装后的 `entry` 与之前的不再是同一个函数。 + * + * 该函数会直接修改传入的 `ComponentMetadata` 并将其返回。 + * + */ +export const newSwitchComponentBuilder = ( + metadata: SwitchMetadata, +): (( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, +) => SwitchComponentMetadata) => { + const extendOptions = newSwitchOptionsMetadataExtender(metadata.switches) + // 返回的 builder + return ( + component: ComponentMetadata | IncompleteSwitchComponentMetadata, + ) => { + const widgetOptions = newSwitchWidgetOptions( + metadata, + component.name, + `${component.displayName}选项`, + ) + + // 若没有 Widget,则注入一个 + if (!component.widget) { + component.widget = newWidget(widgetOptions) + } + // 扩展组件 options + extendOptions(component.options, widgetOptions) + // 包装 entry 函数 + component.entry = newSwitchEntry(component) + + return component as SwitchComponentMetadata } }