mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): support patch style (#126)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
b5e12eaca7
commit
3d10925c53
|
@ -90,13 +90,133 @@ describe('patchProp', () => {
|
||||||
setStyle(el, 'color: red')
|
setStyle(el, 'color: red')
|
||||||
expect(el.style.cssText).toBe('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')
|
const el = document.createElement('div')
|
||||||
setStyle(el, { color: 'red' })
|
setStyle(el, { color: 'red' })
|
||||||
expect(el.style.cssText).toBe('color: red;')
|
expect(el.style.cssText).toBe('color: red;')
|
||||||
setStyle(el, [{ color: 'blue' }, { fontSize: '12px' }])
|
setStyle(el, [{ color: 'blue' }, { fontSize: '12px' }])
|
||||||
expect(el.style.cssText).toBe('color: blue; font-size: 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', () => {
|
describe('setAttr', () => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { isArray, toDisplayString } from '@vue/shared'
|
import { isArray, toDisplayString } from '@vue/shared'
|
||||||
import type { Block, ParentBlock } from './render'
|
import type { Block, ParentBlock } from './render'
|
||||||
|
|
||||||
|
export * from './dom/style'
|
||||||
export * from './dom/prop'
|
export * from './dom/prop'
|
||||||
export * from './dom/event'
|
export * from './dom/event'
|
||||||
export * from './dom/templateRef'
|
export * from './dom/templateRef'
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { currentInstance } from '../component'
|
import { currentInstance } from '../component'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
|
import { setStyle } from './style'
|
||||||
|
|
||||||
export function recordPropMetadata(el: Node, key: string, value: any): any {
|
export function recordPropMetadata(el: Node, key: string, value: any): any {
|
||||||
if (!currentInstance) {
|
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) {
|
export function setAttr(el: Element, key: string, value: any) {
|
||||||
const oldVal = recordPropMetadata(el, key, value)
|
const oldVal = recordPropMetadata(el, key, value)
|
||||||
if (value !== oldVal) {
|
if (value !== oldVal) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue