wkit/src/index.js

475 lines
11 KiB
JavaScript

/**
* { wkit library }
* @author yutent<yutent.io@gmail.com>
* @date 2023/03/07 18:10:43
*/
import {
fixedValue,
parsePropsDeclaration,
boolMap,
KEEP_ALIVE,
KEEP_ALIVE_C,
__finalized__,
__props__,
__changed_props__,
__mounted__,
__pending__
} 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 {
$,
$$,
range,
offset,
outsideClick,
clearOutsideClick
} from './utils.js'
export { html, raw, css, svg, bind, unbind, nextTick, fire, hyphen, camelize }
function safely(callback, ...args) {
try {
callback && callback.apply(this, args)
} catch (err) {
console.error(err)
}
}
// 简单的类名解析
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 function when(condition, trueCase = '', falseCase = '') {
return condition ? trueCase : falseCase
}
// swicth语句的封装
export function which(target, list = [], defaultCase = '') {
for (let [name, content] of list) {
if (target === name) {
return content
}
}
return defaultCase
}
/**
* 双向绑定
* @param key<String> 绑定的字段
* @param el 当前对象, 无需传递, 由框架内部处理
*
*/
export function live(key, el) {
let callback = ev => {
Function('o', 'v', `o.${key} = v`)(this, ev.target.value)
this.$requestUpdate()
}
if (el && !el.__live__) {
bind(el, 'input', callback)
this.$events.input ??= []
this.$events.input.push({ el, listener: callback, options: false })
el.__live__ = true
}
return Function('o', `return o.${key}`)(this)
}
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.concat(this.watches || [], KEEP_ALIVE)
}
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)
}
}
})
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) {
// 保留关键字, 直接跳过
if (k === KEEP_ALIVE || k === KEEP_ALIVE_C) {
continue
}
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 = '', prefix = 'wc') {
let key = prefix + '-' + name
if (!customElements.get(key)) {
customElements.define(key, this)
}
}
keepAlive = false
removed = false
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)
})
Object.defineProperty(this, '$events', {
value: Object.create(null)
})
for (let [prop, options] of this.constructor[__props__]) {
this.#createProperty(prop, options)
// 按W3C规范, 在构造函数内不可设置节点的属性
// 所以这里只记录需要初始化的属性, 在#init()回调中才去修改
this[__changed_props__].set(prop, this[prop])
}
nextTick(_ => this.created())
}
#createProperty(name, options) {
let key = Symbol(name)
let descriptor
if (options.type === Array || options.type === Object) {
let proxyValue = this.#createProxy(null, options, name)
descriptor = {
get() {
return proxyValue
},
set(value) {
proxyValue = this.#createProxy(
fixedValue(value, options),
options,
name
)
this.$requestUpdate(name)
}
}
} 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)
safely.call(this, options.observer, value, oldValue)
}
}
this[key] = options.default
}
Object.defineProperty(this, name, descriptor)
}
#createProxy(obj, options = {}, key) {
return new Proxy(obj || options.default, {
get: (target, prop, receiver) => {
let value = Reflect.get(target, prop, receiver)
// 当访问的值是对象时,需要对这个对象也进行代理
if (typeof value === 'object') {
return this.#createProxy(value, {}, key)
}
return value
},
set: (target, prop, value, receiver) => {
let oldValue = target[prop]
Reflect.set(target, prop, value, receiver)
this.$requestUpdate(key)
safely.call(this, options.observer, value, oldValue)
return true
}
})
}
#init() {
for (let [prop, value] of this[__changed_props__]) {
this.#prop2attr(prop, value)
}
// 这里要清除初始化产生的记录
this[__changed_props__].clear()
this.$requestUpdate()
}
#getPropOptions(name) {
return this.constructor[__props__].get(name)
}
connectedCallback() {
if (this.$animate) {
this.style.display = 'none'
}
this.removed = false
this.#init()
adoptStyles(this.root, this.constructor.styles)
}
disconnectedCallback() {
if (this.keepAlive) {
nextTick(_ => this.deactivated())
} else {
this[__mounted__] = 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)
}
delete $events[name]
}
}
this.removed = true
}
nextTick(_ => this.unmounted())
}
}
// 监听属性变化
attributeChangedCallback(name, old, val) {
if (old === val) {
return
}
if (name === KEEP_ALIVE) {
this.keepAlive = val !== null
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 || options.type === null) {
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)
if (options) {
value = fixedValue(value, options)
if (options.attribute === false || options.type === null) {
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()
}
nextTick(_ => {
if (this.keepAlive) {
this.activated()
}
this.mounted()
this.$requestUpdate()
})
} else {
nextTick(_ => this.updated(props))
}
}
#clearUpdate() {
this[__changed_props__].clear()
this[__pending__] = false
}
// 渲染视图
#render() {
try {
let ast = this.render()
render(ast, this.root, {
host: this
})
} catch (err) {
console.error(err)
}
}
// 几个生命周期回调
created() {}
mounted() {}
activated() {}
deactivated() {}
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%