475 lines
11 KiB
JavaScript
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)
|
|
}
|
|
}
|
JavaScript
97%
HTML
3%