test: ✅ add test specs for css()
method and fix test errors
This commit is contained in:
@ -7,7 +7,7 @@ export namespace Hook {
|
||||
set?: <TElement extends Element>(
|
||||
elem: TElement,
|
||||
styleValue: string | number,
|
||||
) => string | undefined
|
||||
) => string | number | undefined
|
||||
}
|
||||
|
||||
const hooks: Record<string, Definition> = {}
|
||||
@ -19,4 +19,8 @@ export namespace Hook {
|
||||
export function register(styleName: string, hook: Definition) {
|
||||
hooks[styleName] = hook
|
||||
}
|
||||
|
||||
export function unregister(styleName: string) {
|
||||
delete hooks[styleName]
|
||||
}
|
||||
}
|
||||
|
295
packages/x6-vector/src/dom/style/style.test.ts
Normal file
295
packages/x6-vector/src/dom/style/style.test.ts
Normal file
@ -0,0 +1,295 @@
|
||||
import { Dom } from '../dom'
|
||||
import { Hook } from './hook'
|
||||
|
||||
describe('Dom', () => {
|
||||
describe('css', () => {
|
||||
function withCSSContext(css: string, callback: () => void) {
|
||||
const head = document.head || document.getElementsByTagName('head')[0]
|
||||
const style = document.createElement('style')
|
||||
style.setAttribute('type', 'text/css')
|
||||
head.appendChild(style)
|
||||
|
||||
const elem = style as any
|
||||
if (elem.styleSheet) {
|
||||
// This is required for IE8 and below.
|
||||
elem.styleSheet.cssText = css
|
||||
} else {
|
||||
style.appendChild(document.createTextNode(css))
|
||||
}
|
||||
callback()
|
||||
style.parentNode?.removeChild(style)
|
||||
}
|
||||
|
||||
describe('css()', () => {
|
||||
it('should set style with style name-value', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('border', '1px')
|
||||
expect(div.node.getAttribute('style')).toEqual('border: 1px;')
|
||||
})
|
||||
|
||||
it('should set style with style cameCase name-value', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('borderLeft', '1px')
|
||||
expect(div.node.getAttribute('style')).toEqual('border-left: 1px;')
|
||||
})
|
||||
|
||||
it('should auto add unit when set style with style name-value', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('border', 1)
|
||||
expect(div.node.getAttribute('style')).toEqual('border: 1px;')
|
||||
})
|
||||
|
||||
it('should set style with object', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({ border: 1, fontSize: 12 })
|
||||
expect(div.node.getAttribute('style')).toEqual(
|
||||
'border: 1px; font-size: 12px;',
|
||||
)
|
||||
})
|
||||
|
||||
it('should get style by styleName', () => {
|
||||
const div = new Dom('div')
|
||||
expect(div.css('border')).toEqual('')
|
||||
div.css({ border: 1, fontSize: 12 })
|
||||
expect(div.css('border')).toEqual('1px')
|
||||
expect(div.css('fontSize')).toEqual('12px')
|
||||
})
|
||||
|
||||
it('should get computed style by styleName', () => {
|
||||
const body = new Dom(document.body)
|
||||
const div = new Dom('div')
|
||||
body.css({ fontSize: 18 })
|
||||
div.appendTo(body)
|
||||
expect(div.css('fontSize', true)).toEqual('18px')
|
||||
body.attr('style', null)
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should get computed style by styleNames', () => {
|
||||
const body = new Dom(document.body)
|
||||
const div = new Dom('div')
|
||||
body.css({ fontSize: 18, fontWeight: 500 })
|
||||
div.appendTo(body)
|
||||
expect(div.css(['fontSize', 'fontWeight'], true)).toEqual({
|
||||
fontSize: '18px',
|
||||
fontWeight: 500,
|
||||
})
|
||||
body.attr('style', null)
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should return all inline style', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({ border: 1, left: 0 })
|
||||
expect(div.css()).toEqual({
|
||||
left: '0px',
|
||||
border: '1px',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return style by given style names', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({ border: 1, left: 0 })
|
||||
expect(div.css(['left', 'border'])).toEqual({
|
||||
left: '0px',
|
||||
border: '1px',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return all the computed style', () => {
|
||||
const div = new Dom('div')
|
||||
const body = new Dom(document.body)
|
||||
body.css({ fontSize: 18 })
|
||||
div.appendTo(body)
|
||||
expect(div.css(true).fontSize).toEqual('18px')
|
||||
body.attr('style', null)
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should fallback to get inline style if node is not in the document', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({ fontSize: 18 })
|
||||
expect(div.css('fontSize', true)).toEqual('18px')
|
||||
})
|
||||
|
||||
it('should try to convert style value to number', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({ fontWeight: 600 })
|
||||
expect(div.css('fontWeight', true)).toEqual(600)
|
||||
expect(div.css('fontWeight')).toEqual(600)
|
||||
})
|
||||
|
||||
it('should set custom style', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('--custom-key', '10px')
|
||||
expect(div.node.getAttribute('style')).toEqual('--custom-key:10px;')
|
||||
div.attr('style', null)
|
||||
div.css('--customKey', '10px')
|
||||
expect(div.node.getAttribute('style')).toEqual('--custom-key:10px;')
|
||||
})
|
||||
|
||||
it('should get custom style', () => {
|
||||
const div = new Dom('div')
|
||||
div.css({
|
||||
fontSize: 18,
|
||||
'--custom-key': '10px',
|
||||
})
|
||||
expect(div.css('--customKey')).toEqual('10px')
|
||||
expect(div.css('--custom-key')).toEqual('10px')
|
||||
expect(div.css()).toEqual({
|
||||
fontSize: '18px',
|
||||
'--customKey': '10px',
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should not set style on text/comment node', () => {
|
||||
const text = new Dom(document.createTextNode('test') as any)
|
||||
text.css({ fontSize: 18 })
|
||||
expect(text.css('fontSize')).toBeUndefined()
|
||||
expect(text.css()).toEqual({})
|
||||
expect(text.css(true)).toEqual({})
|
||||
|
||||
const comment = new Dom(document.createComment('test') as any)
|
||||
comment.css({ fontSize: 18 })
|
||||
expect(comment.css('fontSize')).toBeUndefined()
|
||||
expect(comment.css()).toEqual({})
|
||||
expect(comment.css(true)).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle "float" specialy', () => {
|
||||
const div = new Dom('div').appendTo(document.body)
|
||||
div.css({ float: 'left' })
|
||||
expect(div.css('float', true)).toEqual('left')
|
||||
expect(div.css('float')).toEqual('left')
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should auto add browser prefix to style name', () => {
|
||||
const div = new Dom('div').appendTo(document.body)
|
||||
div.css('userDrag', 'none')
|
||||
expect(div.node.getAttribute('style')).toEqual(
|
||||
'-webkit-user-drag: none;',
|
||||
)
|
||||
expect(div.css('userDrag', true)).toEqual('none')
|
||||
expect(div.css('userDrag')).toEqual('none')
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should apply hook when get style', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('fontSize', 18)
|
||||
Hook.register('fontSize', {
|
||||
get(node, computed) {
|
||||
return computed ? 16 : 14
|
||||
},
|
||||
})
|
||||
expect(div.css('fontSize', true)).toEqual(16)
|
||||
expect(div.css('fontSize')).toEqual(14)
|
||||
Hook.unregister('fontSize')
|
||||
})
|
||||
|
||||
it('should apply hook when set style', () => {
|
||||
const div = new Dom('div')
|
||||
Hook.register('fontSize', {
|
||||
set() {
|
||||
return 20
|
||||
},
|
||||
})
|
||||
div.css('fontSize', 18)
|
||||
expect(div.css('fontSize')).toEqual('20px')
|
||||
})
|
||||
})
|
||||
|
||||
describe('show()', () => {
|
||||
it('should not change the "display" style when it\'s visible', () => {
|
||||
const div = new Dom('div')
|
||||
div.show()
|
||||
expect(div.node.getAttribute('style')).toBeNull()
|
||||
expect(div.css('display')).toEqual('')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
|
||||
it('should show the node', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('display', 'none')
|
||||
div.show()
|
||||
expect(div.node.getAttribute('style')).toEqual('')
|
||||
expect(div.css('display')).toEqual('')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
|
||||
it('should recover the old value of "display" style', () => {
|
||||
const div = new Dom('div')
|
||||
div.css('display', 'inline-block')
|
||||
|
||||
div.hide()
|
||||
expect(div.css('display')).toEqual('none')
|
||||
expect(div.visible()).toBeFalse()
|
||||
|
||||
div.show()
|
||||
expect(div.css('display')).toEqual('inline-block')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
|
||||
it('should force visible when it was hidden in tree', () => {
|
||||
withCSSContext('.hidden { display: none; }', () => {
|
||||
const div = new Dom('div').appendTo(document.body)
|
||||
div.addClass('hidden')
|
||||
expect(div.visible()).toBeFalse()
|
||||
div.show()
|
||||
expect(div.visible()).toBeTrue()
|
||||
expect(div.css('display')).toEqual('block')
|
||||
div.remove()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('hide()', () => {
|
||||
it('should hide the node', () => {
|
||||
const div = new Dom('div')
|
||||
div.hide()
|
||||
expect(div.css('display')).toEqual('none')
|
||||
expect(div.visible()).toBeFalse()
|
||||
div.show()
|
||||
expect(div.css('display')).toEqual('')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
})
|
||||
|
||||
describe('visible()', () => {
|
||||
it('should return false when it was hidden in tree', () => {
|
||||
withCSSContext('.hidden { display: none; }', () => {
|
||||
const div = new Dom('div').appendTo(document.body)
|
||||
div.addClass('hidden')
|
||||
expect(div.visible()).toBeFalse()
|
||||
|
||||
div.remove()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggle()', () => {
|
||||
it('should toggle visible state of node', () => {
|
||||
const div = new Dom('div')
|
||||
div.toggle()
|
||||
expect(div.css('display')).toEqual('none')
|
||||
expect(div.visible()).toBeFalse()
|
||||
div.toggle()
|
||||
expect(div.css('display')).toEqual('')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
|
||||
it('should set visible state of node', () => {
|
||||
const div = new Dom('div')
|
||||
div.hide()
|
||||
div.toggle(false)
|
||||
expect(div.css('display')).toEqual('none')
|
||||
expect(div.visible()).toBeFalse()
|
||||
|
||||
div.toggle(true)
|
||||
expect(div.css('display')).toEqual('')
|
||||
expect(div.visible()).toBeTrue()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -36,8 +36,17 @@ export class Style<TElement extends Element> extends Base<TElement> {
|
||||
* is `true`, otherwise returns inline style properties.
|
||||
*/
|
||||
css(computed?: boolean): CSSProperties
|
||||
css(styleName: string, computed?: boolean): string | number
|
||||
css(styleNames: string[], computed?: boolean): Record<string, string | number>
|
||||
css(styleName: string, styleValue: string | number): this
|
||||
css(
|
||||
style?: boolean | CSSPropertyName | CSSPropertyName[] | CSSProperties,
|
||||
style?:
|
||||
| boolean
|
||||
| CSSPropertyName
|
||||
| CSSPropertyName[]
|
||||
| CSSProperties
|
||||
| string
|
||||
| string[],
|
||||
value?: string | number | null | boolean,
|
||||
) {
|
||||
const node = (this.node as any) as HTMLElement
|
||||
@ -46,10 +55,13 @@ export class Style<TElement extends Element> extends Base<TElement> {
|
||||
if (style == null || typeof style === 'boolean') {
|
||||
if (style) {
|
||||
const result: CSSProperties = {}
|
||||
const computedStyle = Util.getComputedStyle(node)
|
||||
Array.from(computedStyle).forEach((key: MockedCSSName) => {
|
||||
result[key] = Util.css(node, key, computedStyle)
|
||||
})
|
||||
if (Util.isValidNode(node)) {
|
||||
const computedStyle = Util.getComputedStyle(node)
|
||||
Array.from(computedStyle).forEach((key) => {
|
||||
const name = Util.camelCase(key) as MockedCSSName
|
||||
result[name] = Util.css(node, key, computedStyle)
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -60,10 +72,12 @@ export class Style<TElement extends Element> extends Base<TElement> {
|
||||
if (Array.isArray(style)) {
|
||||
const result: CSSProperties = {}
|
||||
if (value) {
|
||||
const computedStyle = Util.getComputedStyle(node)
|
||||
style.forEach((name: MockedCSSName) => {
|
||||
result[name] = Util.css(node, name, computedStyle)
|
||||
})
|
||||
if (Util.isValidNode(node)) {
|
||||
const computedStyle = Util.getComputedStyle(node)
|
||||
style.forEach((name: MockedCSSName) => {
|
||||
result[name] = Util.css(node, name, computedStyle)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
style.forEach((name: MockedCSSName) => {
|
||||
result[name] = Util.style(node, name)
|
||||
@ -81,14 +95,12 @@ export class Style<TElement extends Element> extends Base<TElement> {
|
||||
}
|
||||
|
||||
// get style for property
|
||||
if (typeof value == null || typeof value === 'boolean') {
|
||||
if (value == null || typeof value === 'boolean') {
|
||||
return value ? Util.css(node, style) : Util.style(node, style)
|
||||
}
|
||||
|
||||
// set style for property
|
||||
if (typeof value === 'string') {
|
||||
Util.style(node, style, value)
|
||||
}
|
||||
Util.style(node, style, value)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Global } from '../../global'
|
||||
import { camelCase, isInDocument } from '../../util'
|
||||
import { isInDocument } from '../../util'
|
||||
import { Hook } from './hook'
|
||||
import { CSSProperties } from './types'
|
||||
|
||||
@ -29,6 +29,20 @@ export namespace Util {
|
||||
return result !== undefined ? `${result}` : result
|
||||
}
|
||||
|
||||
export function isValidNode<TElement extends Element>(node: TElement) {
|
||||
// Don't set styles on text and comment nodes
|
||||
if (!node || node.nodeType === 3 || node.nodeType === 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
const style = ((node as any) as HTMLElement).style
|
||||
if (!style) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isCustomStyleName(styleName: string) {
|
||||
return styleName.indexOf('--') === 0
|
||||
}
|
||||
@ -37,8 +51,37 @@ export namespace Util {
|
||||
// Used by the css & effects modules.
|
||||
// Support: IE <=9 - 11+
|
||||
// Microsoft forgot to hump their vendor prefix
|
||||
export function cssCamelCase(str: string) {
|
||||
return camelCase(str.replace(/^-ms-/, 'ms-'))
|
||||
export function camelCase(str: string) {
|
||||
const to = (s: string) =>
|
||||
s
|
||||
.replace(/^-ms-/, 'ms-')
|
||||
.replace(/-([a-z])/g, (input) => input[1].toUpperCase())
|
||||
|
||||
if (isCustomStyleName(str)) {
|
||||
return `--${to(str.substring(2))}`
|
||||
}
|
||||
|
||||
return to(str)
|
||||
}
|
||||
|
||||
export function kebabCase(str: string) {
|
||||
return (
|
||||
str
|
||||
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||
// vendor
|
||||
.replace(/^([A-Z])/g, '-$1')
|
||||
.toLowerCase()
|
||||
.replace(/^ms-/, '-ms-')
|
||||
)
|
||||
}
|
||||
|
||||
export function tryConvertToNumber(value: string | number) {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
|
||||
const numReg = /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
|
||||
return numReg.test(value) ? +value : value
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +193,7 @@ export namespace Util {
|
||||
name: string,
|
||||
presets?: Record<string, string> | CSSStyleDeclaration,
|
||||
) {
|
||||
const styleName = cssCamelCase(name)
|
||||
const styleName = camelCase(name)
|
||||
const isCustom = isCustomStyleName(name)
|
||||
|
||||
// Make sure that we're working with the right name. We don't
|
||||
@ -170,10 +213,10 @@ export namespace Util {
|
||||
|
||||
// Otherwise, if a way to get the computed value exists, use that
|
||||
if (val === undefined) {
|
||||
val = getComputedStyleValue(node, name, presets)
|
||||
val = getComputedStyleValue(node, kebabCase(name), presets)
|
||||
}
|
||||
|
||||
return val
|
||||
return tryConvertToNumber(val)
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,33 +258,26 @@ export namespace Util {
|
||||
name?: string,
|
||||
value?: string | number,
|
||||
) {
|
||||
// Don't set styles on text and comment nodes
|
||||
if (!node || node.nodeType === 3 || node.nodeType === 8) {
|
||||
if (!isValidNode(node)) {
|
||||
return typeof name === 'undefined' ? {} : undefined
|
||||
}
|
||||
|
||||
const style = ((node as any) as HTMLElement).style
|
||||
if (!style) {
|
||||
return typeof name === 'undefined' ? {} : undefined
|
||||
}
|
||||
const styleDeclaration = ((node as any) as HTMLElement).style
|
||||
|
||||
if (typeof name === 'undefined') {
|
||||
const result: CSSProperties = {}
|
||||
style.cssText
|
||||
styleDeclaration.cssText
|
||||
.split(/\s*;\s*/)
|
||||
.filter((str) => str.length > 0)
|
||||
.forEach((str) => {
|
||||
const parts = str.split(/\s*:\s*/)
|
||||
result[cssCamelCase(parts[0]) as MockedCSSName] = Util.style(
|
||||
node,
|
||||
parts[0],
|
||||
)
|
||||
result[camelCase(parts[0]) as MockedCSSName] = style(node, parts[0])
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Make sure that we're working with the right name
|
||||
const styleName = cssCamelCase(name)
|
||||
const styleName = camelCase(name)
|
||||
const isCustom = isCustomStyleName(name)
|
||||
|
||||
// Make sure that we're working with the right name. We don't
|
||||
@ -256,7 +292,7 @@ export namespace Util {
|
||||
|
||||
// Setting a value
|
||||
if (value !== undefined) {
|
||||
let val = normalizeValue(name, value, isCustom)
|
||||
let val = value
|
||||
// If a hook was provided, use that value, otherwise just set the specified value
|
||||
let setting = true
|
||||
if (hook && hook.set) {
|
||||
@ -268,27 +304,30 @@ export namespace Util {
|
||||
}
|
||||
|
||||
if (setting) {
|
||||
val = normalizeValue(name, val, isCustom)
|
||||
if (name === 'float') {
|
||||
name = 'cssFloat' // eslint-disable-line
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
style.setProperty(name, val)
|
||||
styleDeclaration.setProperty(kebabCase(name), val)
|
||||
} else {
|
||||
style[name as MockedCSSName] = val
|
||||
styleDeclaration[name as MockedCSSName] = val
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let ret: string | number | undefined
|
||||
// If a hook was provided get the non-computed value from there
|
||||
if (hook && hook.get) {
|
||||
const ret = hook.get(node, false)
|
||||
if (ret !== undefined) {
|
||||
return ret
|
||||
}
|
||||
ret = hook.get(node, false)
|
||||
}
|
||||
|
||||
// Otherwise just get the value from the style object
|
||||
return style.getPropertyValue(name)
|
||||
if (ret === undefined) {
|
||||
ret = styleDeclaration.getPropertyValue(kebabCase(name))
|
||||
}
|
||||
|
||||
return tryConvertToNumber(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -316,7 +355,10 @@ export namespace Util {
|
||||
const doc = node.ownerDocument || Global.document
|
||||
const temp = doc.body.appendChild(doc.createElement(nodeName))
|
||||
display = css(temp, 'display') as string
|
||||
doc.removeChild(temp)
|
||||
|
||||
if (temp.parentNode) {
|
||||
temp.parentNode.removeChild(temp)
|
||||
}
|
||||
|
||||
if (display === 'none') {
|
||||
display = 'block'
|
||||
@ -337,23 +379,27 @@ export namespace Util {
|
||||
}
|
||||
|
||||
const display = style.display
|
||||
let val: string | undefined
|
||||
if (show) {
|
||||
// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
|
||||
// check is required in this first loop unless we have a nonempty display value (either
|
||||
// inline or about-to-be-restored)
|
||||
if (display === 'none') {
|
||||
const value = cache.get(node)
|
||||
if (!value) {
|
||||
val = cache.get(node)
|
||||
if (!val) {
|
||||
style.display = ''
|
||||
}
|
||||
}
|
||||
|
||||
// for cascade-hidden
|
||||
if (style.display === '' && isHiddenWithinTree(node)) {
|
||||
style.display = getDefaultDisplay(node)
|
||||
val = getDefaultDisplay(node)
|
||||
}
|
||||
} else if (display !== 'none') {
|
||||
style.display = 'none'
|
||||
val = 'none'
|
||||
// Remember what we're overwriting
|
||||
cache.set(node, display)
|
||||
}
|
||||
|
||||
if (val != null) {
|
||||
style.display = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,5 @@ export const ucfirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
||||
export const lcfirst = (s: string) => s.charAt(0).toLowerCase() + s.slice(1)
|
||||
|
||||
export function camelCase(str: string) {
|
||||
return str.replace(/-([a-z])/g, (input, letter: string) =>
|
||||
letter.toUpperCase(),
|
||||
)
|
||||
return str.replace(/-([a-z])/g, (input) => input[1].toUpperCase())
|
||||
}
|
||||
|
Reference in New Issue
Block a user