wkit/src/index.js

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)
}
}
一个简单易用、功能完善的用于开发`web components`的轻量级开发库。模板解析基于`lit-html`二次开发
JavaScript 97%
HTML 3%