Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdk:theme): add cdk theme support #1739

Merged
merged 1 commit into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ export * from '@idux/cdk/popper'
export * from '@idux/cdk/portal'
export * from '@idux/cdk/resize'
export * from '@idux/cdk/scroll'
export * from '@idux/cdk/theme'
export * from '@idux/cdk/utils'
export * from '@idux/cdk/version'
12 changes: 12 additions & 0 deletions packages/cdk/theme/demo/Basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
order: 0
title:
zh: 基本使用
en: Basic usage
---

## zh

主题切换

## en
22 changes: 22 additions & 0 deletions packages/cdk/theme/demo/Basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div>
<h2>Platform:</h2>
<p>Is Browser: {{ isBrowser }}</p>
<p>Is Blink: {{ isBlink }}</p>
<p>Is Webkit: {{ isWebKit }}</p>
<p>Is iOS: {{ isIOS }}</p>
<p>Is Android: {{ isAndroid }}</p>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

import { isAndroid, isBlink, isBrowser, isIOS, isWebKit } from '@idux/cdk/platform'

export default defineComponent({
setup() {
return { isBrowser, isBlink, isWebKit, isIOS, isAndroid }
},
})
</script>
1 change: 1 addition & 0 deletions packages/cdk/theme/docs/Api.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## API
11 changes: 11 additions & 0 deletions packages/cdk/theme/docs/Index.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
category: cdk
type:
title: Theme
subtitle: 主题
cover:
---

`@idux-vue2/cdk/theme` 提供了一组用于获取、更新主题的响应式工具

需要对不同主题做兼容性处理时使用
8 changes: 8 additions & 0 deletions packages/cdk/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

export * from './src/useTheme'
179 changes: 179 additions & 0 deletions packages/cdk/theme/src/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import { type ComputedRef, onMounted } from 'vue'

import { debounce } from 'lodash-es'

import { Logger, createSharedComposable, tryOnScopeDispose, useState } from '@idux/cdk/utils'

export interface UseThemeParams<Theme extends string> {
/**
* CSS Selector for the target element applying to
*
* @default 'body''
*/
selector?: string | Element | null | undefined
/**
* HTML attribute applying the target element
*
* @default 'color-schema''
*/
attribute?: string

/**
* The initial value of the theme
*/
defaultTheme?: Theme

/**
* The map of the theme value and the corresponding attribute value
*/
themes?: Theme[]

/**
* Debounce time for updating the theme value
*/
debounceTime?: number
}

export interface UseThemeReturns<Theme extends string> {
theme: ComputedRef<Theme>
changeTheme: (theme: Theme) => void
}

/**
* reactivy theme hook
*/
export const useTheme = createSharedComposable(
<Theme extends string>(options: UseThemeParams<Theme> = {}): UseThemeReturns<Theme> => {
const {
selector = 'body',
attribute = 'color-schema',
defaultTheme = 'default' as Theme,
themes = [],
debounceTime = 300,
} = options

const mergedThemes = [...themes]

const requiredOptions: Required<UseThemeParams<Theme>> = {
selector,
attribute,
themes: mergedThemes,
defaultTheme,
debounceTime,
}

const [theme, setTheme] = useState<Theme>(getHTMLAttrs(requiredOptions))
const updateThemeAttrs = () => {
updateHTMLAttrs(
Object.assign({}, requiredOptions, {
defaultTheme: theme.value,
}),
)
}

updateThemeAttrs()

const changeTheme = (newTheme: Theme) => {
setTheme(newTheme)
updateThemeAttrs()
}

const onDomChange = debounce(() => {
const oldTheme = theme.value
const newTheme = getHTMLAttrs(
Object.assign({}, requiredOptions, {
defaultTheme: theme.value,
}),
)
// 这里需要手动节流一下,否则会重复更新 theme 的值
if (oldTheme !== newTheme) {
changeTheme(newTheme)
}
}, debounceTime)

let observer: MutationObserver
onMounted(() => {
if (observer) {
return
}
observer = new MutationObserver(onDomChange)
const el = getEl(selector)
if (el) {
observer.observe(el, {
attributes: true,
attributeFilter: [attribute],
})
}
})
tryOnScopeDispose(() => {
observer?.disconnect
})

return {
theme,
changeTheme,
}
},
)

function getEl(selector: UseThemeParams<string>['selector']) {
const el = typeof selector === 'string' ? window?.document.querySelector(selector) : selector

if (!el && __DEV__) {
Logger.warn('cdk/theme', `The element holding the theme cannot be found.`)
}

return el
}

/**
* Update the HTML attribute of the target element
*/
function updateHTMLAttrs<Theme extends string>(options: Required<UseThemeParams<Theme>>) {
const { selector, attribute, themes, defaultTheme } = options

const el = getEl(selector)
if (!el) {
return
}

if (attribute !== 'class') {
el.setAttribute(attribute, defaultTheme)
return
}

const currentCls = defaultTheme.split(/\s+/g)
Object.values(themes)
.flatMap(i => String(i || '').split(/\s/g))
.filter(Boolean)
.forEach(v => {
if (currentCls.includes(v)) {
el.classList.add(v)
} else {
el.classList.remove(v)
}
})
}

function getHTMLAttrs<Theme extends string>(options: Required<UseThemeParams<Theme>>): Theme {
const { selector, attribute, themes, defaultTheme } = options
const el = getEl(selector)
if (!el) {
return defaultTheme
}

const val = el.getAttribute(attribute) ?? defaultTheme
if (attribute !== 'class') {
return val as Theme
}

const currentCls = val.split(/\s+/g)
return (themes.filter(targetValue => currentCls.includes(targetValue)).at(0) ?? defaultTheme) as Theme
}