387 lines
8.6 KiB
JavaScript
387 lines
8.6 KiB
JavaScript
/**
|
|
* { wkit library }
|
|
* @author yutent<yutent.io@gmail.com>
|
|
* @date 2023/03/07 18:10:43
|
|
*/
|
|
import {
|
|
fixedValue,
|
|
parsePropsDeclaration,
|
|
boolMap,
|
|
__finalized__,
|
|
__props__,
|
|
__changed_props__,
|
|
__mounted__,
|
|
__pending__,
|
|
__children__
|
|
} from './constants.js'
|
|
import { css, adoptStyles } from './css.js'
|
|
import { render, html, svg, raw } from './html.js'
|
|
import { animate, MODES } from './anim.js'
|
|
import { nextTick, fire, bind, unbind, hyphen, camelize } from './utils.js'
|
|
|
|
export { $, $$, offset, outsideClick, clearOutsideClick } from './utils.js'
|
|
export { html, raw, css, svg, bind, unbind, nextTick, fire, hyphen, camelize }
|
|
|
|
// 简单的类名解析
|
|
export function classMap(data = {}) {
|
|
let output = ''
|
|
for (let k in data) {
|
|
if (data[k]) {
|
|
output += ' ' + k
|
|
}
|
|
}
|
|
return output.slice(1)
|
|
}
|
|
// 简单的样式解析
|
|
export function styleMap(data = {}) {
|
|
let output = ''
|
|
for (let k in data) {
|
|
if (data[k] || data[k] === 0) {
|
|
output += hyphen(k) + ':' + data[k] + ';'
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
|
|
export class Component extends HTMLElement {
|
|
/**
|
|
* 声明可监听变化的属性列表
|
|
* @return list<Array>
|
|
*/
|
|
static get observedAttributes() {
|
|
let list = []
|
|
|
|
this.finalize()
|
|
this.parseAnim()
|
|
|
|
this[__props__].forEach((options, prop) => {
|
|
list.push(options.attrName)
|
|
})
|
|
return list
|
|
}
|
|
|
|
static parseAnim() {
|
|
if (this.hasOwnProperty('animation')) {
|
|
let {
|
|
type = 'fade',
|
|
duration,
|
|
custom,
|
|
immediate = false
|
|
} = this.animation
|
|
let fromto = MODES[type] || MODES.fade
|
|
if (custom) {
|
|
fromto = custom
|
|
}
|
|
Object.defineProperty(this.prototype, '$animate', {
|
|
value(out) {
|
|
if (this[__mounted__]) {
|
|
return animate.call(this, duration, fromto, out)
|
|
}
|
|
},
|
|
enumerable: false
|
|
})
|
|
this.prototype.$animate.immediate = immediate
|
|
delete this.animation
|
|
}
|
|
}
|
|
|
|
// 处理静态声明
|
|
static finalize() {
|
|
if (this[__finalized__]) {
|
|
return false
|
|
}
|
|
this[__finalized__] = true
|
|
|
|
this[__props__] = new Map()
|
|
|
|
if (this.hasOwnProperty('props')) {
|
|
for (let k in this.props) {
|
|
let options = parsePropsDeclaration(this.props[k])
|
|
let attrName = k.toLowerCase()
|
|
|
|
options.attrName = attrName
|
|
|
|
if (boolMap[attrName]) {
|
|
k = boolMap[attrName]
|
|
} else {
|
|
options.attrName = hyphen(k)
|
|
k = camelize(k)
|
|
}
|
|
this[__props__].set(k, options)
|
|
}
|
|
}
|
|
|
|
delete this.props
|
|
}
|
|
|
|
static reg(name = '') {
|
|
name = 'wc-' + name
|
|
if (!customElements.get(name)) {
|
|
customElements.define(name, this)
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
super()
|
|
this[__pending__] = false
|
|
this[__mounted__] = false
|
|
// 这里提前定义一次, 是为了在connectedCallback之前, 就已有赋值时报错的bug
|
|
this[__changed_props__] = new Map() // 记录一次渲染周期内变化的属性
|
|
this.host = this
|
|
this.root = this.shadowRoot || this.attachShadow({ mode: 'open' })
|
|
this.root.ownHost = this
|
|
|
|
Object.defineProperty(this, '$refs', {
|
|
value: Object.create(null),
|
|
enumerable: false
|
|
})
|
|
Object.defineProperty(this, '$events', {
|
|
value: Object.create(null),
|
|
enumerable: false
|
|
})
|
|
|
|
for (let [prop, options] of this.constructor[__props__]) {
|
|
this.createProperty(prop, options)
|
|
this.$requestUpdate(prop)
|
|
}
|
|
|
|
this.created()
|
|
}
|
|
|
|
createProperty(name, options) {
|
|
let key = Symbol(name)
|
|
let descriptor
|
|
|
|
if (options.type === Array || options.type === Object) {
|
|
let proxyValue = this.#createProxy(name, options)
|
|
descriptor = {
|
|
get() {
|
|
return proxyValue
|
|
},
|
|
set(value) {
|
|
proxyValue = this.#createProxy(name, options, value)
|
|
this.$requestUpdate(name)
|
|
},
|
|
enumerable: false
|
|
}
|
|
} else {
|
|
descriptor = {
|
|
get() {
|
|
return this[key]
|
|
},
|
|
set(value) {
|
|
let oldValue = this[key]
|
|
value = fixedValue(value, options)
|
|
if (oldValue === value) {
|
|
return
|
|
}
|
|
this[key] = value
|
|
this.$requestUpdate(name)
|
|
if (options.observer) {
|
|
options.observer.call(this, value, oldValue)
|
|
}
|
|
},
|
|
enumerable: false
|
|
}
|
|
this[key] = options.default
|
|
}
|
|
|
|
Object.defineProperty(this, name, descriptor)
|
|
}
|
|
|
|
#createProxy(name, options, newValue) {
|
|
return new Proxy(newValue || options.default, {
|
|
set: (target, prop, value) => {
|
|
if (prop === 'length' && options.type === Array) {
|
|
return true
|
|
}
|
|
let oldValue = target[prop]
|
|
target[prop] = value
|
|
|
|
this.$requestUpdate(name)
|
|
if (options.observer) {
|
|
options.observer.call(this, value, oldValue)
|
|
}
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
#init() {
|
|
// 这里重新赋值一次, 用于清除掉因为 observer 修正产生的变化
|
|
this[__changed_props__] = new Map()
|
|
this.$requestUpdate()
|
|
}
|
|
|
|
#getPropOptions(name) {
|
|
return this.constructor[__props__].get(name)
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.$animate) {
|
|
this.style.display = 'none'
|
|
}
|
|
this.#init()
|
|
adoptStyles(this.root, this.constructor.styles)
|
|
|
|
this[__children__]?.setConnected(true)
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this[__children__]?.setConnected(false)
|
|
|
|
if (!document.body?.contains(this)) {
|
|
let $events = this.$events
|
|
if ($events) {
|
|
for (let name in $events) {
|
|
for (let it of $events[name]) {
|
|
unbind(it.el, name, it.listener, it.options)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.unmounted()
|
|
}
|
|
// 监听属性变化
|
|
attributeChangedCallback(name, old, val) {
|
|
if (old === val) {
|
|
return
|
|
}
|
|
this.#attr2prop(name, val, old)
|
|
}
|
|
|
|
/**
|
|
* 处理需要显式渲染到html标签上的属性
|
|
* 复杂类型永不显式渲染
|
|
* @param name<String>
|
|
* @param value<String|Boolean|Number>
|
|
*/
|
|
#prop2attr(name, value) {
|
|
let options = this.#getPropOptions(name)
|
|
let attrName = options.attrName
|
|
|
|
if (options.attribute === false) {
|
|
this.removeAttribute(attrName)
|
|
return
|
|
}
|
|
|
|
switch (options.type) {
|
|
case Number:
|
|
case String:
|
|
if (value === null) {
|
|
this.removeAttribute(attrName)
|
|
} else {
|
|
this.setAttribute(attrName, value)
|
|
}
|
|
break
|
|
|
|
case Boolean:
|
|
if (value === null || value === false) {
|
|
this.removeAttribute(attrName)
|
|
} else {
|
|
this.setAttribute(attrName, '')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通过setAttribute设置的值, 需要转成props
|
|
* @param name<String>
|
|
* @param value<String|Boolean|Number>
|
|
*/
|
|
#attr2prop(name, value, old) {
|
|
let _name = boolMap[name]
|
|
let options, propName
|
|
|
|
if (_name) {
|
|
name = _name
|
|
}
|
|
propName = camelize(name)
|
|
|
|
options = this.#getPropOptions(propName)
|
|
value = fixedValue(value, options)
|
|
|
|
if (options.attribute === false) {
|
|
if (value === null) {
|
|
return
|
|
}
|
|
if (value === this[propName]) {
|
|
this.removeAttribute(name)
|
|
return
|
|
}
|
|
}
|
|
|
|
this[propName] = value
|
|
}
|
|
|
|
// 请求更新
|
|
$requestUpdate(name) {
|
|
if (name !== void 0) {
|
|
this[__changed_props__].set(name, this[name])
|
|
this.#prop2attr(name, this[name])
|
|
}
|
|
|
|
if (this[__pending__] === false) {
|
|
this[__pending__] = true
|
|
nextTick(_ => this.#confirmUpdate())
|
|
}
|
|
}
|
|
|
|
// 确认更新到视图
|
|
#confirmUpdate() {
|
|
let props = this[__changed_props__]
|
|
this.#render()
|
|
this.#feedback(props)
|
|
this.#clearUpdate()
|
|
}
|
|
|
|
// 更新回调反馈
|
|
#feedback(props) {
|
|
// 初始化时不触发updated回调
|
|
if (this[__mounted__] === false) {
|
|
this[__mounted__] = true
|
|
if (this.$animate?.immediate) {
|
|
this.$animate()
|
|
}
|
|
this.mounted()
|
|
} else {
|
|
this.updated(props)
|
|
}
|
|
}
|
|
|
|
#clearUpdate() {
|
|
this[__changed_props__] = new Map()
|
|
this[__pending__] = false
|
|
}
|
|
|
|
// 渲染视图
|
|
#render() {
|
|
let ast = this.render()
|
|
this[__children__] = render(ast, this.root, {
|
|
host: this,
|
|
isConnected: !this[__mounted__] && this.isConnected
|
|
})
|
|
}
|
|
// 几个生命周期回调
|
|
created() {}
|
|
mounted() {}
|
|
unmounted() {}
|
|
updated() {}
|
|
render() {
|
|
return html`<slot></slot>`
|
|
}
|
|
|
|
$on(type, callback, options) {
|
|
return bind(this, type, callback, options)
|
|
}
|
|
|
|
$off(type, callback, options) {
|
|
unbind(this, type, callback, options)
|
|
}
|
|
|
|
$emit(type, data = {}, stop = false) {
|
|
return fire(this, type, data, stop)
|
|
}
|
|
}
|
JavaScript
97%
HTML
3%