feat(runtime-vapor): support patch style (#126)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
FireBushtree 2024-02-10 21:31:44 +08:00 committed by GitHub
parent b5e12eaca7
commit 3d10925c53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 229 additions and 12 deletions

View File

@ -90,13 +90,133 @@ describe('patchProp', () => {
setStyle(el, 'color: red')
expect(el.style.cssText).toBe('color: red;')
})
test.fails('shoud set style with object and array property', () => {
test('should work with camelCase', () => {
const el = document.createElement('div')
setStyle(el, { fontSize: '12px' })
expect(el.style.cssText).toBe('font-size: 12px;')
})
test('shoud set style with object and array property', () => {
const el = document.createElement('div')
setStyle(el, { color: 'red' })
expect(el.style.cssText).toBe('color: red;')
setStyle(el, [{ color: 'blue' }, { fontSize: '12px' }])
expect(el.style.cssText).toBe('color: blue; font-size: 12px;')
})
test('should remove if falsy value', () => {
const el = document.createElement('div')
setStyle(el, { color: undefined, borderRadius: null })
expect(el.style.cssText).toBe('')
setStyle(el, { color: 'red' })
expect(el.style.cssText).toBe('color: red;')
setStyle(el, { color: undefined, borderRadius: null })
expect(el.style.cssText).toBe('')
})
test('should work with !important', () => {
const el = document.createElement('div')
setStyle(el, { color: 'red !important' })
expect(el.style.cssText).toBe('color: red !important;')
})
test('should work with camelCase and !important', () => {
const el = document.createElement('div')
setStyle(el, { fontSize: '12px !important' })
expect(el.style.cssText).toBe('font-size: 12px !important;')
})
test('should work with multiple entries', () => {
const el = document.createElement('div')
setStyle(el, { color: 'red', marginRight: '10px' })
expect(el.style.getPropertyValue('color')).toBe('red')
expect(el.style.getPropertyValue('margin-right')).toBe('10px')
})
test('should patch with falsy style value', () => {
const el = document.createElement('div')
setStyle(el, { width: '100px' })
expect(el.style.cssText).toBe('width: 100px;')
setStyle(el, { width: 0 })
expect(el.style.cssText).toBe('width: 0px;')
})
test('should remove style attribute on falsy value', () => {
const el = document.createElement('div')
setStyle(el, { width: '100px' })
expect(el.style.cssText).toBe('width: 100px;')
setStyle(el, { width: undefined })
expect(el.style.cssText).toBe('')
setStyle(el, { width: '100px' })
expect(el.style.cssText).toBe('width: 100px;')
setStyle(el, null)
expect(el.hasAttribute('style')).toBe(false)
expect(el.style.cssText).toBe('')
})
test('should warn for trailing semicolons', () => {
const el = document.createElement('div')
setStyle(el, { color: 'red;' })
expect(
`Unexpected semicolon at the end of 'color' style value: 'red;'`,
).toHaveBeenWarned()
setStyle(el, { '--custom': '100; ' })
expect(
`Unexpected semicolon at the end of '--custom' style value: '100; '`,
).toHaveBeenWarned()
})
test('should not warn for trailing semicolons', () => {
const el = document.createElement('div')
setStyle(el, { '--custom': '100\\;' })
expect(el.style.getPropertyValue('--custom')).toBe('100\\;')
})
test('should work with shorthand properties', () => {
const el = document.createElement('div')
setStyle(el, { borderBottom: '1px solid red', border: '1px solid green' })
expect(el.style.border).toBe('1px solid green')
expect(el.style.borderBottom).toBe('1px solid green')
})
// JSDOM doesn't support custom properties on style object so we have to
// mock it here.
function mockElementWithStyle() {
const store: any = {}
return {
style: {
display: '',
WebkitTransition: '',
setProperty(key: string, val: string) {
store[key] = val
},
getPropertyValue(key: string) {
return store[key]
},
},
}
}
test('should work with css custom properties', () => {
const el = mockElementWithStyle()
setStyle(el as any, { '--theme': 'red' })
expect(el.style.getPropertyValue('--theme')).toBe('red')
})
test('should auto vendor prefixing', () => {
const el = mockElementWithStyle()
setStyle(el as any, { transition: 'all 1s' })
expect(el.style.WebkitTransition).toBe('all 1s')
})
test('should work with multiple values', () => {
const el = mockElementWithStyle()
setStyle(el as any, { display: ['-webkit-box', '-ms-flexbox', 'flex'] })
expect(el.style.display).toBe('flex')
})
})
describe('setAttr', () => {

View File

@ -1,6 +1,7 @@
import { isArray, toDisplayString } from '@vue/shared'
import type { Block, ParentBlock } from './render'
export * from './dom/style'
export * from './dom/prop'
export * from './dom/event'
export * from './dom/templateRef'

View File

@ -11,6 +11,7 @@ import {
} from '@vue/shared'
import { currentInstance } from '../component'
import { warn } from '../warning'
import { setStyle } from './style'
export function recordPropMetadata(el: Node, key: string, value: any): any {
if (!currentInstance) {
@ -34,17 +35,6 @@ export function setClass(el: Element, value: any) {
}
}
export function setStyle(el: HTMLElement, value: any) {
const prev = recordPropMetadata(el, 'style', (value = normalizeStyle(value)))
if (value !== prev && (value || prev)) {
if (typeof value === 'string') {
el.style.cssText = value
} else {
// TODO
}
}
}
export function setAttr(el: Element, key: string, value: any) {
const oldVal = recordPropMetadata(el, key, value)
if (value !== oldVal) {

View File

@ -0,0 +1,106 @@
import {
camelize,
capitalize,
hyphenate,
isArray,
isString,
normalizeStyle,
} from '@vue/shared'
import { warn } from '../warning'
import { recordPropMetadata } from './prop'
export function setStyle(el: HTMLElement, value: any) {
const prev = recordPropMetadata(el, 'style', (value = normalizeStyle(value)))
patchStyle(el, prev, value)
}
// TODO copied from packages/runtime-dom/src/modules/style.ts
type Style = string | Record<string, string | string[]> | null
function patchStyle(el: Element, prev: Style, next: Style) {
const style = (el as HTMLElement).style
const isCssString = isString(next)
if (next && !isCssString) {
if (prev && !isString(prev)) {
for (const key in prev) {
if (next[key] == null) {
setStyleValue(style, key, '')
}
}
}
for (const key in next) {
setStyleValue(style, key, next[key])
}
} else {
if (isCssString) {
// TODO: combine with v-show
if (prev !== next) {
style.cssText = next
}
} else if (prev) {
el.removeAttribute('style')
}
}
}
const semicolonRE = /[^\\];\s*$/
const importantRE = /\s*!important$/
function setStyleValue(
style: CSSStyleDeclaration,
name: string,
val: string | string[],
) {
if (isArray(val)) {
val.forEach(v => setStyleValue(style, name, v))
} else {
if (val == null) val = ''
if (__DEV__) {
if (semicolonRE.test(val)) {
warn(
`Unexpected semicolon at the end of '${name}' style value: '${val}'`,
)
}
}
if (name.startsWith('--')) {
// custom property definition
style.setProperty(name, val)
} else {
const prefixed = autoPrefix(style, name)
if (importantRE.test(val)) {
// !important
style.setProperty(
hyphenate(prefixed),
val.replace(importantRE, ''),
'important',
)
} else {
style[prefixed as any] = val
}
}
}
}
const prefixes = ['Webkit', 'Moz', 'ms']
const prefixCache: Record<string, string> = {}
function autoPrefix(style: CSSStyleDeclaration, rawName: string): string {
const cached = prefixCache[rawName]
if (cached) {
return cached
}
let name = camelize(rawName)
if (name !== 'filter' && name in style) {
return (prefixCache[rawName] = name)
}
name = capitalize(name)
for (let i = 0; i < prefixes.length; i++) {
const prefixed = prefixes[i] + name
if (prefixed in style) {
return (prefixCache[rawName] = prefixed)
}
}
return rawName
}