/** * { wkit library } * @author yutent * @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 */ 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 * @param value */ #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 * @param value */ #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`` } $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) } }