Compare commits

..

No commits in common. "master" and "1.5.9" have entirely different histories.

12 changed files with 459 additions and 968 deletions

5
.gitignore vendored
View File

@ -1,9 +1,12 @@
*.min.js *.min.js
.httpserver .httpserver
index.html
test.js
.vscode .vscode
node_modules/ node_modules/
dist/ dist/
*.sublime-project
*.sublime-workspace
package-lock.json package-lock.json

View File

@ -1 +0,0 @@
test/

View File

@ -1,30 +1,12 @@
## Wkit ## 9号UI组件库的核心
> 一个简单易用、功能完善的用于开发`web components`的轻量级开发库。模板解析基于`lit-html`二次开发
![downloads](https://img.shields.io/npm/dt/wkit.svg)
![version](https://img.shields.io/npm/v/wkit.svg)
### 开发文档 ### 开发文档
[开发文档](https://git.wkit.fun/bytedo/wkit/wiki) [开发文档](https://github.com/bd-js/core/wiki)
### 我们的特色
- `@xxx=yyy` 事件绑定, 可以任意节点上直接使用类似`vue事件绑定`的这种写法, 就可实现事件绑定。组件被移出文档时, 事件会**自动销毁**, 无须手动销毁。
- `@click.stop=xxx` 事件绑定支持修饰符
- `:xxx=yyy`, 属性绑定, 类似`vue`,在属性前加上`:`, 该属性不再使用`setAttribute()`, 而是直接赋值, 可满足你需要传递复杂数据结构的需求。
- `#animation={type}`, 开箱即用的动画配置, 内置6种动画`fade(默认), scale, bounce, micro-bounce, rotate, slide`
- `ref=xxx`, 类似`vue`的节点标识, 可方便的在`mounted()`之后通过 `this.$refs.xxx` 来访问该节点
- 内置特色的双向绑定方法`live()`
- 用内置的`bind()`方法来给当前组件绑定事件时, 组件移除时事件也会自动销毁,无需手动销毁。
- 灵活的`props`定义
### 示例 ### 示例
```js ```js
// alias wkit='//jscdn.ink/wkit/latest/index.js' import { css, html, Component } from '//jscdn.ink/@bd/core/latest/index.js'
import { css, html, Component } from 'wkit'
class Hello extends Component { class Hello extends Component {
@ -38,7 +20,7 @@ class Hello extends Component {
bar: String // 简写 bar: String // 简写
} }
// 若需要支持 scss语法, 则需要使用构建工具编译 // 若需要支持 scss, 则需要使用 @bd/wcui-cli,预处理
// 可支持数组 // 可支持数组
static styles = css` static styles = css`
button { color: #09f; } button { color: #09f; }
@ -66,12 +48,7 @@ class Hello extends Component {
} }
if (!customElements.get('wc-hello')) { customElements.define('wc-hello', Hello)
customElements.define('wc-hello', Hello)
}
// 和上面的写法等价
// 如果不希望修改前缀`wc-`的话, 可以直接调用内置的reg方法注册组件即可。
Hello.reg('hello')
/* /*
@ -85,7 +62,3 @@ Hello.reg('hello')
### 开源协议
- BSD 3-Clause License (Lit框架的html模板解析的源码)
- MIT (除Lit代码之外的所有代码)

View File

@ -1,8 +1,8 @@
{ {
"name": "wkit", "name": "@bd/core",
"version": "1.12.0", "version": "1.5.9",
"type": "module", "type": "module",
"description": "A library for building fast, lightweight web components.", "description": "百搭UI组件库的核心",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [
"dist/*" "dist/*"
@ -14,16 +14,14 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.wkit.fun/bytedo/wkit.git" "url": "git+https://github.com/bd-js/core.git"
}, },
"keywords": [ "keywords": [
"wkit", "bd.js",
"bdjs",
"bd-ui",
"wcui", "wcui",
"web-components", "bd-core"
"native",
"cross-framework",
"html",
"css"
], ],
"devDependencies": { "devDependencies": {
"esbuild": "^0.17.11" "esbuild": "^0.17.11"

View File

@ -1,48 +0,0 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2023/03/29 10:28:16
*/
export function animate(duration = 200, fromto = [], out = false) {
if (out === false && this.style.display === 'none') {
this.style.display = ''
}
let res = this.animate(fromto, {
duration,
direction: out ? 'reverse' : 'normal',
fill: 'forwards'
})
return new Promise(resolve => {
res.addEventListener(
'finish',
_ => {
if (out) {
this.style.display = 'none'
}
res.cancel()
resolve()
},
{ once: true }
)
})
}
export const MODES = {
fade: [{ opacity: 0 }, { opacity: 1 }],
scale: [{ transform: 'scale(0)' }, { transform: 'scale(1)' }],
'micro-bounce': [
{ opacity: 0, transform: 'scale(0.8)' },
{ transform: 'scale(1.05)', opacity: 1 },
{ transform: 'scale(1)' }
],
slide: [
{ opacity: 0, transform: 'translateY(-50px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
bounce: [
{ transform: 'scale(0)' },
{ transform: 'scale(1.2)' },
{ transform: 'scale(1)' }
],
rotate: [{ transform: 'rotate(0)' }, { transform: 'rotate(360deg)' }]
}

View File

@ -35,17 +35,25 @@ const boolMap = Object.create(null)
export { boolMap } export { boolMap }
export const KEEP_ALIVE = 'keep-alive' // hyphen
export const KEEP_ALIVE_C = 'keepAlive' // camelize
export const WC_PART = Symbol('wc_path') export const WC_PART = Symbol('wc_path')
export const NO_CHANGE = Symbol('wc-noChange')
export const NOTHING = Symbol('wc-nothing') export const NOTHING = Symbol('wc-nothing')
export const __finalized__ = Symbol('finalized') export const __finalized__ = Symbol('finalized')
export const __render__ = Symbol('render')
export const __init__ = Symbol('init')
export const __props__ = Symbol('props') export const __props__ = Symbol('props')
export const __changed_props__ = Symbol('changed_props') export const __changed_props__ = Symbol('changed_props')
export const __mounted__ = Symbol('mounted') export const __mounted__ = Symbol('mounted')
export const __feedback__ = Symbol('feedback')
export const __pending__ = Symbol('pending') export const __pending__ = Symbol('pending')
export const __prop2attr__ = Symbol('prop2attr')
export const __attr2prop__ = Symbol('attr2prop')
export const __clear_update__ = Symbol('clearupdate')
export const __children__ = Symbol('children')
export const __updated__ = Symbol('updated')
export const __in_tree__ = Symbol('intree')
export const RESET_CSS_STYLE = `* {box-sizing: border-box;margin: 0;padding: 0;}::before,::after {box-sizing: border-box;}::selection {background: var(--selection-background, var(--color-plain-a));color: var(--selection-color, inherit);}` export const RESET_CSS_STYLE = `* {box-sizing: border-box;margin: 0;padding: 0;}::before,::after {box-sizing: border-box;}`
function getDefaultValue(type) { function getDefaultValue(type) {
switch (type) { switch (type) {
@ -79,26 +87,8 @@ function getType(v) {
? { type: Array, default: v } ? { type: Array, default: v }
: { type: Object, default: v } : { type: Object, default: v }
case 'string':
let type = String
let attribute = true
if (v.includes('!')) {
if (v.startsWith('str!')) {
v = v.slice(4)
} else if (v.startsWith('num!')) {
type = Number
v = +v.slice(4) || 0
} else if (v.startsWith('bool!')) {
type = Boolean
v = v.slice(5)
v = v !== 'false' && v !== ''
}
attribute = false
}
return { type, default: v, attribute }
default: default:
return { type: null, default: '' } return { type: String, default: v + '' }
} }
} }
@ -128,10 +118,6 @@ export function parsePropsDeclaration(options) {
options = { type: Array } options = { type: Array }
break break
case String:
options = { type: String }
break
default: default:
options = getType(options) options = getType(options)
break break
@ -139,9 +125,7 @@ export function parsePropsDeclaration(options) {
options.default = options.hasOwnProperty('default') options.default = options.hasOwnProperty('default')
? options.default ? options.default
: getDefaultValue(options.type) : getDefaultValue(options.type)
options.attribute = options.hasOwnProperty('attribute') options.attribute = true
? options.attribute
: true
return options return options
} }
@ -175,9 +159,6 @@ export function fixedValue(value, options) {
} }
} }
case null:
return value
default: default:
return value === null || value === void 0 ? null : value + '' return value === null || value === void 0 ? null : value + ''
} }

View File

@ -37,7 +37,7 @@ export function adoptStyles(root, styles = '') {
styles = (RESET_CSS_STYLE + styles.join(' ')).trim() styles = (RESET_CSS_STYLE + styles.join(' ')).trim()
sheet.replaceSync(styles) sheet.replaceSync(styles)
if (root.adoptedStyleSheets) { if (root.adoptedStyleSheets) {
root.adoptedStyleSheets = [sheet] root.adoptedStyleSheets.push(sheet)
} else { } else {
root.appendChild(sheet.elem) root.appendChild(sheet.elem)
} }

View File

@ -1,35 +1,28 @@
/** import { boolMap, WC_PART, NO_CHANGE, NOTHING } from './constants.js'
* {模板解析, 基于 lit-html 二次开发}
* @author yutent<yutent.io@gmail.com>
* @date 2023/03/7 16:36:59
*/
import { boolMap, WC_PART, NOTHING } from './constants.js' const boundAttributeSuffix = '$wc$'
import { animate, MODES } from './anim.js' const marker = `wc$${String(Math.random()).slice(9)}$`
import { nextTick, noop } from './utils.js' const markerMatch = '?' + marker
const nodeMarker = `<${markerMatch}>`
const BIND_ATTR_SUFFIX = '{{$wkit$}}' const createMarker = (v = '') => document.createComment(v)
const MARKER = `{{^wkit${String(Math.random()).slice(-8)}^}}`
const MARKER_MATCH = '?' + MARKER
const NODE_MARKER = `<${MARKER_MATCH}>`
// 是否原始值
const isPrimitive = value => const isPrimitive = value =>
value === null || (typeof value !== 'object' && typeof value !== 'function') value === null || (typeof value != 'object' && typeof value != 'function')
const isArray = Array.isArray const isArray = Array.isArray
const isIterable = value => const isIterable = value =>
value ? isArray(value) || typeof value[Symbol.iterator] === 'function' : false isArray(value) ||
typeof (value === null || value === void 0
? false
: value[Symbol.iterator]) === 'function'
const SPACE_CHAR = `[ \n\f\r]` const SPACE_CHAR = `[ \n\f\r]`
const ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]` const ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]`
const NAME_CHAR = `[^\\s"'>=/]` const NAME_CHAR = `[^\\s"'>=/]`
const TEXT_END_REGEX = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g const textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g
const COMMENT_START = 1 const COMMENT_START = 1
const TAG_NAME = 2 const TAG_NAME = 2
const DYNAMIC_TAG_NAME = 3 const DYNAMIC_TAG_NAME = 3
const COMMENT_END_REGEXP = /-->/g const commentEndRegex = /-->/g
const COMMENT_END_REGEXP_2 = />/g const comment2EndRegex = />/g
const TAG_END_REGEXP = new RegExp( const tagEndRegex = new RegExp(
`>|${SPACE_CHAR}(?:(${NAME_CHAR}+)(${SPACE_CHAR}*=${SPACE_CHAR}*(?:${ATTR_VALUE_CHAR}|("|')|))|$)`, `>|${SPACE_CHAR}(?:(${NAME_CHAR}+)(${SPACE_CHAR}*=${SPACE_CHAR}*(?:${ATTR_VALUE_CHAR}|("|')|))|$)`,
'g' 'g'
) )
@ -37,9 +30,9 @@ const ENTIRE_MATCH = 0
const ATTRIBUTE_NAME = 1 const ATTRIBUTE_NAME = 1
const SPACES_AND_EQUALS = 2 const SPACES_AND_EQUALS = 2
const QUOTE_CHAR = 3 const QUOTE_CHAR = 3
const SINGLE_QUOTE_REGEXP = /'/g const singleQuoteAttrEndRegex = /'/g
const DOUBLE_QUOTE_REGEXP = /"/g const doubleQuoteAttrEndRegex = /"/g
const RAW_TEXT_ELEM_REGEXP = /^(?:script|style|textarea|title)$/i const rawTextElement = /^(?:script|style|textarea|title)$/i
const HTML_RESULT = 1 const HTML_RESULT = 1
const SVG_RESULT = 2 const SVG_RESULT = 2
const ATTRIBUTE_PART = 1 const ATTRIBUTE_PART = 1
@ -48,7 +41,7 @@ const EVENT_PART = 5
const ELEMENT_PART = 6 const ELEMENT_PART = 6
const COMMENT_PART = 7 const COMMENT_PART = 7
const TEMPLATE_CACHE = new Map() const templateCache = new WeakMap()
const walker = document.createTreeWalker(document, 129, null, false) const walker = document.createTreeWalker(document, 129, null, false)
function getTemplateHtml(strings, type) { function getTemplateHtml(strings, type) {
@ -56,7 +49,7 @@ function getTemplateHtml(strings, type) {
let attrNames = [] let attrNames = []
let html2 = type === SVG_RESULT ? '<svg>' : '' let html2 = type === SVG_RESULT ? '<svg>' : ''
let rawTextEndRegex let rawTextEndRegex
let regex = TEXT_END_REGEX let regex = textEndRegex
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
let s = strings[i] let s = strings[i]
let attrNameEndIndex = -1 let attrNameEndIndex = -1
@ -70,26 +63,25 @@ function getTemplateHtml(strings, type) {
break break
} }
lastIndex = regex.lastIndex lastIndex = regex.lastIndex
if (regex === textEndRegex) {
if (regex === TEXT_END_REGEX) {
if (match[COMMENT_START] === '!--') { if (match[COMMENT_START] === '!--') {
regex = COMMENT_END_REGEXP regex = commentEndRegex
} else if (match[COMMENT_START] !== void 0) { } else if (match[COMMENT_START] !== void 0) {
regex = COMMENT_END_REGEXP_2 regex = comment2EndRegex
} else if (match[TAG_NAME] !== void 0) { } else if (match[TAG_NAME] !== void 0) {
if (RAW_TEXT_ELEM_REGEXP.test(match[TAG_NAME])) { if (rawTextElement.test(match[TAG_NAME])) {
rawTextEndRegex = new RegExp(`</${match[TAG_NAME]}`, 'g') rawTextEndRegex = new RegExp(`</${match[TAG_NAME]}`, 'g')
} }
regex = TAG_END_REGEXP regex = tagEndRegex
} else if (match[DYNAMIC_TAG_NAME] !== void 0) { } else if (match[DYNAMIC_TAG_NAME] !== void 0) {
regex = TAG_END_REGEXP regex = tagEndRegex
} }
} else if (regex === TAG_END_REGEXP) { } else if (regex === tagEndRegex) {
if (match[ENTIRE_MATCH] === '>') { if (match[ENTIRE_MATCH] === '>') {
regex = regex =
rawTextEndRegex !== null && rawTextEndRegex !== void 0 rawTextEndRegex !== null && rawTextEndRegex !== void 0
? rawTextEndRegex ? rawTextEndRegex
: TEXT_END_REGEX : textEndRegex
attrNameEndIndex = -1 attrNameEndIndex = -1
} else if (match[ATTRIBUTE_NAME] === void 0) { } else if (match[ATTRIBUTE_NAME] === void 0) {
attrNameEndIndex = -2 attrNameEndIndex = -2
@ -98,76 +90,60 @@ function getTemplateHtml(strings, type) {
attrName = match[ATTRIBUTE_NAME] attrName = match[ATTRIBUTE_NAME]
regex = regex =
match[QUOTE_CHAR] === void 0 match[QUOTE_CHAR] === void 0
? TAG_END_REGEXP ? tagEndRegex
: match[QUOTE_CHAR] === '"' : match[QUOTE_CHAR] === '"'
? DOUBLE_QUOTE_REGEXP ? doubleQuoteAttrEndRegex
: SINGLE_QUOTE_REGEXP : singleQuoteAttrEndRegex
} }
} else if ( } else if (
regex === DOUBLE_QUOTE_REGEXP || regex === doubleQuoteAttrEndRegex ||
regex === SINGLE_QUOTE_REGEXP regex === singleQuoteAttrEndRegex
) { ) {
regex = TAG_END_REGEXP regex = tagEndRegex
} else if ( } else if (regex === commentEndRegex || regex === comment2EndRegex) {
regex === COMMENT_END_REGEXP || regex = textEndRegex
regex === COMMENT_END_REGEXP_2
) {
regex = TEXT_END_REGEX
} else { } else {
regex = TAG_END_REGEXP regex = tagEndRegex
rawTextEndRegex = void 0 rawTextEndRegex = void 0
} }
} }
let end = let end =
regex === TAG_END_REGEXP && strings[i + 1].startsWith('/>') ? ' ' : '' regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : ''
html2 += html2 +=
regex === TEXT_END_REGEX regex === textEndRegex
? s + NODE_MARKER ? s + nodeMarker
: attrNameEndIndex >= 0 : attrNameEndIndex >= 0
? (attrNames.push(attrName), ? (attrNames.push(attrName),
s.slice(0, attrNameEndIndex) + s.slice(0, attrNameEndIndex) +
BIND_ATTR_SUFFIX + boundAttributeSuffix +
s.slice(attrNameEndIndex)) + s.slice(attrNameEndIndex)) +
MARKER + marker +
end end
: s + : s +
MARKER + marker +
(attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end) (attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end)
} }
let htmlResult = let htmlResult =
html2 + (strings[len] || '<?>') + (type === SVG_RESULT ? '</svg>' : '') html2 + (strings[len] || '<?>') + (type === SVG_RESULT ? '</svg>' : '')
if (!isArray(strings) || !strings.hasOwnProperty('raw')) { if (!Array.isArray(strings) || !strings.hasOwnProperty('raw')) {
throw new Error('invalid html ast') let message = 'invalid template strings array'
throw new Error(message)
} }
return [htmlResult, attrNames] return [htmlResult, attrNames]
} }
function createElement(v = '') {
let el = document.createElement('template')
el.innerHTML = v
return el
}
function createMarker(v = '') {
return document.createComment(v)
}
class Template { class Template {
parts = [] constructor({ strings, ['__dom_type__']: type }, options) {
this.parts = []
constructor({ strings, values, __dom_type__: type }, options) {
let node let node
let nodeIndex = 0 let nodeIndex = 0
let attrNameIndex = 0 let attrNameIndex = 0
let partCount = strings.length - 1 let partCount = strings.length - 1
let parts = this.parts let parts = this.parts
let [html2, attrNames] = getTemplateHtml(strings, type) let [html2, attrNames] = getTemplateHtml(strings, type)
this.el = Template.createElement(html2)
this.el = createElement(html2)
walker.currentNode = this.el.content walker.currentNode = this.el.content
if (type === SVG_RESULT) { if (type === SVG_RESULT) {
let content = this.el.content let content = this.el.content
let svgElement = content.firstChild let svgElement = content.firstChild
@ -181,33 +157,27 @@ class Template {
let attrsToRemove = [] let attrsToRemove = []
for (let name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
if (name.endsWith(BIND_ATTR_SUFFIX) || name.startsWith(MARKER)) { if (
name.endsWith(boundAttributeSuffix) ||
name.startsWith(marker)
) {
let realName = attrNames[attrNameIndex++] let realName = attrNames[attrNameIndex++]
attrsToRemove.push(name) attrsToRemove.push(name)
if (realName !== void 0) { if (realName !== void 0) {
let value = node.getAttribute( let value = node.getAttribute(
realName.toLowerCase() + BIND_ATTR_SUFFIX realName.toLowerCase() + boundAttributeSuffix
) )
let statics = value.split(MARKER) let statics = value.split(marker)
let m = /([#:@])?(.*)/.exec(realName) let m = /([:@])?(.*)/.exec(realName)
let decorates = []
if (m[1] === '@' && m[2].includes('.')) {
decorates = m[2].split('.')
m[2] = decorates.shift()
}
parts.push({ parts.push({
type: ATTRIBUTE_PART, type: ATTRIBUTE_PART,
index: nodeIndex, index: nodeIndex,
name: m[2], name: m[2],
strings: statics, strings: statics,
decorates,
ctor: ctor:
m[1] === ':' m[1] === ':'
? PropertyPart ? PropertyPart
: m[1] === '#' && m[2] === 'animation'
? AnimPart
: m[1] === '@' : m[1] === '@'
? EventPart ? EventPart
: AttributePart : AttributePart
@ -224,8 +194,8 @@ class Template {
node.removeAttribute(name) node.removeAttribute(name)
} }
} }
if (RAW_TEXT_ELEM_REGEXP.test(node.tagName)) { if (rawTextElement.test(node.tagName)) {
let strings2 = node.textContent.split(MARKER) let strings2 = node.textContent.split(marker)
let lastIndex = strings2.length - 1 let lastIndex = strings2.length - 1
if (lastIndex > 0) { if (lastIndex > 0) {
node.textContent = '' node.textContent = ''
@ -239,66 +209,108 @@ class Template {
} }
} else if (node.nodeType === 8) { } else if (node.nodeType === 8) {
let data = node.data let data = node.data
if (data === MARKER_MATCH) { if (data === markerMatch) {
parts.push({ type: CHILD_PART, index: nodeIndex }) parts.push({ type: CHILD_PART, index: nodeIndex })
} else { } else {
let i = -1 let i = -1
while ((i = node.data.indexOf(MARKER, i + 1)) !== -1) { while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
parts.push({ type: COMMENT_PART, index: nodeIndex }) parts.push({ type: COMMENT_PART, index: nodeIndex })
i += MARKER.length - 1 i += marker.length - 1
} }
} }
} }
nodeIndex++ nodeIndex++
} }
} }
static createElement(html2) {
let el = document.createElement('template')
el.innerHTML = html2
return el
}
}
function resolveDirective(part, value, parent = part, attributeIndex) {
if (value === NO_CHANGE) {
return value
}
let currentDirective =
attributeIndex !== void 0
? parent.__directives?.[attributeIndex]
: parent.__directive
let nextDirectiveConstructor = isPrimitive(value)
? void 0
: value['_$litDirective$']
if (currentDirective?.constructor !== nextDirectiveConstructor) {
currentDirective._$notifyDirectiveConnectionChanged?.call(
currentDirective,
false
)
if (nextDirectiveConstructor === void 0) {
currentDirective = void 0
} else {
currentDirective = new nextDirectiveConstructor(part)
currentDirective._$initialize(part, parent, attributeIndex)
}
if (attributeIndex !== void 0) {
if (!parent.__directives) {
parent.__directives = []
}
parent.__directives[attributeIndex] = currentDirective
} else {
parent.__directive = currentDirective
}
}
if (currentDirective !== void 0) {
value = resolveDirective(
part,
currentDirective._$resolve(part, value.values),
currentDirective,
attributeIndex
)
}
return value
} }
class TemplateInstance { class TemplateInstance {
constructor(template, parent) { constructor(template, parent) {
this.$parts = [] this._parts = []
this.$template = template this._$disconnectableChildren = void 0
this.$parent = parent this._$template = template
this._$parent = parent
} }
get parentNode() { get parentNode() {
return this.$parent.parentNode return this._$parent.parentNode
} }
get _$isConnected() {
return this._$parent._$isConnected
}
_clone(options) {
let {
el: { content },
parts
} = this._$template
#checkRef(node, walker, options) { let fragment = (options?.creationScope || document).importNode(
content,
true
)
walker.currentNode = fragment
let node = walker.nextNode()
let nodeIndex = 0
let partIndex = 0
let templatePart = parts[0]
// 没有动态绑定时, 查一遍是否有ref属性
if (templatePart === void 0) {
do { do {
if (node && node.nodeType === 1 && node.getAttribute('ref')) { if (node.nodeType === 1 && node.getAttribute('ref')) {
options.host.$refs[node.getAttribute('ref')] = node options.host.$refs[node.getAttribute('ref')] = node
node.removeAttribute('ref') node.removeAttribute('ref')
} }
} while ((node = walker.nextNode()) !== null) } while ((node = walker.nextNode()) !== null)
}
clone(options) {
let {
el: { content },
parts
} = this.$template
let fragment = document.importNode(content, true)
let nodeIndex = 0
let partIndex = 0
let templatePart = parts[0]
let node = null
walker.currentNode = fragment
node = walker.nextNode()
// 没有动态绑定时, 查一遍是否有ref属性
if (templatePart === void 0) {
this.#checkRef(node, walker, options)
} else { } else {
while (templatePart !== void 0) { while (templatePart !== void 0) {
if (node.nodeType === 1 && node.getAttribute('ref')) {
if (options.host.$refs) {
options.host.$refs[node.getAttribute('ref')] = node
node.removeAttribute('ref')
}
}
if (nodeIndex === templatePart.index) { if (nodeIndex === templatePart.index) {
let part let part
if (templatePart.type === CHILD_PART) { if (templatePart.type === CHILD_PART) {
@ -308,14 +320,18 @@ class TemplateInstance {
node, node,
templatePart.name, templatePart.name,
templatePart.strings, templatePart.strings,
templatePart.decorates,
this, this,
options options
) )
} else if (templatePart.type === ELEMENT_PART) { } else if (templatePart.type === ELEMENT_PART) {
part = new ElementPart(node, this, options) part = new ElementPart(node, this, options)
} }
this.$parts.push(part) if (node.nodeType === 1 && node.getAttribute('ref')) {
if (options.host.$refs) {
options.host.$refs[node.getAttribute('ref')] = node
}
}
this._parts.push(part)
templatePart = parts[++partIndex] templatePart = parts[++partIndex]
} }
if ( if (
@ -328,22 +344,19 @@ class TemplateInstance {
nodeIndex++ nodeIndex++
} }
} }
// 再检查剩下没有动态绑定的节点
this.#checkRef(node, walker, options)
} }
return fragment return fragment
} }
_update(values) {
update(values) {
let i = 0 let i = 0
for (let part of this.$parts) { for (let part of this._parts) {
if (part !== void 0) { if (part !== void 0) {
if (part.strings !== void 0) { if (part.strings !== void 0) {
part.$setValue(values, i) part._$setValue(values, part, i)
i += part.strings.length - 2 i += part.strings.length - 2
} else { } else {
part.$setValue(values[i]) part._$setValue(values[i])
} }
} }
i++ i++
@ -352,116 +365,118 @@ class TemplateInstance {
} }
class ChildPart { class ChildPart {
type = CHILD_PART
#value = NOTHING
constructor(startNode, endNode, parent, options) { constructor(startNode, endNode, parent, options) {
this.startNode = startNode this.type = CHILD_PART
this.endNode = endNode this._$committedValue = NOTHING
this.$parent = parent this._$disconnectableChildren = void 0
this._$startNode = startNode
this._$endNode = endNode
this._$parent = parent
this.options = options this.options = options
this.__isConnected = options?.isConnected || true
}
get _$isConnected() {
return this._$parent?._$isConnected || this.__isConnected
} }
get parentNode() { get parentNode() {
let parentNode = this.startNode.parentNode let parentNode = this._$startNode.parentNode
let parent = this.$parent let parent = this._$parent
if (parent !== void 0 && parentNode.nodeType === 11) { if (parent !== void 0 && parentNode.nodeType === 11) {
parentNode = parent.parentNode parentNode = parent.parentNode
} }
return parentNode return parentNode
} }
get startNode() {
$setValue(value, clear) { return this._$startNode
}
get endNode() {
return this._$endNode
}
_$setValue(value, directiveParent = this) {
value = resolveDirective(this, value, directiveParent)
if (isPrimitive(value)) { if (isPrimitive(value)) {
if (value === NOTHING || value == null || value === '') { if (value === NOTHING || value == null || value === '') {
if (this.#value !== NOTHING) { if (this._$committedValue !== NOTHING) {
this.#clear() this.#clear()
} }
this.#value = NOTHING this._$committedValue = NOTHING
} else if (value !== this.#value) { } else if (value !== this._$committedValue && value !== NO_CHANGE) {
this.#commitText(value) this._commitText(value)
} }
} else if (value.__dom_type__ !== void 0) { } else if (value['__dom_type__'] !== void 0) {
this.#commitTemplateResult(value) this._commitTemplateResult(value)
} else if (value.nodeType !== void 0) { } else if (value.nodeType !== void 0) {
this.#commitNode(value) this._commitNode(value)
} else if (isIterable(value)) { } else if (isIterable(value)) {
this.#commitIterable(value) this._commitIterable(value)
} else { } else {
this.#commitText(value) this._commitText(value)
}
if (clear) {
nextTick(_ => this.#clearBindings())
} }
} }
_insert(node, ref = this._$endNode) {
#insert(node, target = this.endNode) { return this._$startNode.parentNode.insertBefore(node, ref)
return this.startNode.parentNode.insertBefore(node, target)
} }
_commitNode(value) {
#commitNode(value) { if (this._$committedValue !== value) {
if (this.#value !== value) {
this.#clear() this.#clear()
this.#value = this.#insert(value) this._$committedValue = this._insert(value)
} }
} }
_commitText(value) {
#commitText(value) { if (
if (this.#value !== NOTHING && isPrimitive(this.#value)) { this._$committedValue !== NOTHING &&
let node = this.startNode.nextSibling isPrimitive(this._$committedValue)
) {
let node = this._$startNode.nextSibling
node.data = value node.data = value
} else { } else {
this.#commitNode(document.createTextNode(value)) this._commitNode(document.createTextNode(value))
} }
this.#value = value this._$committedValue = value
} }
_commitTemplateResult(result) {
#commitTemplateResult(result) { let { values, ['__dom_type__']: type } = result
let { values, __dom_type__: type } = result
let template = let template =
typeof type === 'number' typeof type === 'number'
? this.#getTemplate(result) ? this.#getTemplate(result)
: (type.el === void 0 && (type.el = createElement(type.h)), type) : (type.el === void 0 && (type.el = Template.createElement(type.h)),
type)
if (this.#value?.$template === template) { if (this._$committedValue?._$template === template) {
this.#value.update(values) this._$committedValue._update(values)
} else { } else {
let instance = new TemplateInstance(template, this) let instance = new TemplateInstance(template, this)
let fragment = instance.clone(this.options) let fragment = instance._clone(this.options)
instance.update(values) instance._update(values)
this.#commitNode(fragment) this._commitNode(fragment)
this.#value = instance this._$committedValue = instance
} }
} }
#getTemplate(result) { #getTemplate(result) {
let key = result.strings.join() let template = templateCache.get(result.strings)
let template = TEMPLATE_CACHE.get(key)
if (template === void 0) { if (template === void 0) {
template = new Template(result, this.options) template = new Template(result, this.options)
TEMPLATE_CACHE.set(key, template) templateCache.set(result.strings, template)
} }
return template return template
} }
_commitIterable(value) {
#commitIterable(value) { if (!isArray(this._$committedValue)) {
if (!isArray(this.#value)) { this._$committedValue = []
this.#value = []
this.#clear() this.#clear()
} }
let itemParts = this.#value let itemParts = this._$committedValue
let partIndex = 0 let partIndex = 0
let itemPart let itemPart
for (let item of value) { for (let item of value) {
if (partIndex === itemParts.length) { if (partIndex === itemParts.length) {
itemParts.push( itemParts.push(
(itemPart = new ChildPart( (itemPart = new ChildPart(
this.#insert(createMarker()), this._insert(createMarker()),
this.#insert(createMarker()), this._insert(createMarker()),
this, this,
this.options this.options
)) ))
@ -469,125 +484,115 @@ class ChildPart {
} else { } else {
itemPart = itemParts[partIndex] itemPart = itemParts[partIndex]
} }
itemPart.$setValue(item) itemPart._$setValue(item)
partIndex++ partIndex++
} }
if (partIndex < itemParts.length) { if (partIndex < itemParts.length) {
this.#clear(itemPart && itemPart.endNode.nextSibling, partIndex) this.#clear(itemPart && itemPart._$endNode.nextSibling, partIndex)
itemParts.length = partIndex itemParts.length = partIndex
} }
} }
#clear(start = this.startNode.nextSibling, from) { #clear(start = this._$startNode.nextSibling, from) {
while (start && start !== this.endNode) { this._$notifyConnectionChanged?.call(this, false, true, from)
while (start && start !== this._$endNode) {
let node = start.nextSibling let node = start.nextSibling
start.remove() start.remove()
start = node start = node
} }
} }
setConnected(isConnected) {
/** if (this._$parent === void 0) {
* 清除已被移除的节点事件 this.__isConnected = isConnected
*/ this._$notifyConnectionChanged?.call(this, isConnected)
#clearBindings() {
let host = this.options.host
let events = host.$events
for (let ev in events) {
for (let i = -1, it; (it = events[ev][++i]); ) {
if (!host.root.contains(it.el)) {
it.el.removeEventListener(this.name, it.listener, it.options)
events[ev][i] = null
}
}
events[ev] = events[ev].filter(e => e !== null)
} }
} }
} }
// 常规属性 // 常规属性
class AttributePart { class AttributePart {
type = ATTRIBUTE_PART constructor(element, name, strings, parent, options) {
this.type = ATTRIBUTE_PART
#value = NOTHING this._$committedValue = NOTHING
this._$disconnectableChildren = void 0
constructor(element, name, strings, decorates, parent, options = {}) {
this.element = element this.element = element
this.name = name this.name = name
this.decorates = decorates this._$parent = parent
this.$parent = parent
this.options = options this.options = options
if (strings.length > 2 || strings[0] !== '' || strings[1] !== '') { if (strings.length > 2 || strings[0] !== '' || strings[1] !== '') {
this.#value = Array(strings.length - 1).fill(null) this._$committedValue = new Array(strings.length - 1).fill(new String())
this.strings = strings this.strings = strings
} else {
this._$committedValue = NOTHING
} }
} }
get tagName() { get tagName() {
return this.element.tagName return this.element.tagName
} }
get _$isConnected() {
$setValue(value, valueIndex) { return this._$parent._$isConnected
let strings = this.strings
let changed = false
if (typeof value === 'function') {
value = value(this.element)
} }
_$setValue(value, directiveParent = this, valueIndex, noCommit) {
let strings = this.strings
let change = false
if (strings === void 0) { if (strings === void 0) {
changed = !isPrimitive(value) || value !== this.#value value = resolveDirective(this, value, directiveParent, 0)
if (changed) { change =
this.#value = value !isPrimitive(value) ||
(value !== this._$committedValue && value !== NO_CHANGE)
if (change) {
this._$committedValue = value
} }
} else { } else {
let values = value let values = value
value = strings[0] value = strings[0]
for (let i = 0; i < strings.length - 1; i++) { for (let i = 0; i < strings.length - 1; i++) {
let v = values[valueIndex + i] let v = resolveDirective(
this,
changed || (changed = !isPrimitive(v) || v !== this.#value[i]) values[valueIndex + i],
directiveParent,
i
)
if (v === NO_CHANGE) {
v = this._$committedValue[i]
}
change || (change = !isPrimitive(v) || v !== this._$committedValue[i])
if (v === NOTHING) { if (v === NOTHING) {
value = NOTHING value = NOTHING
} else if (value !== NOTHING) { } else if (value !== NOTHING) {
value += (v !== null && v !== void 0 ? v : '') + strings[i + 1] value += (v !== null && v !== void 0 ? v : '') + strings[i + 1]
} }
this.#value[i] = v this._$committedValue[i] = v
} }
} }
// value属性做特殊处理, 解决输入框无法清空的问题 if (change && !noCommit) {
if (changed || this.name === 'value') {
this.commitValue(value) this.commitValue(value)
} }
} }
commitValue(value) { commitValue(value) {
let elem = this.element let isBoolAttr = boolMap[this.name]
let attr = this.name
let isBoolAttr = boolMap[attr]
// ref属性不渲染到节点上 // ref属性不渲染到节点上
if (attr === 'ref') { if (this.name === 'ref') {
this.options.host.$refs[value] = elem this.options.host.$refs[value] = this.element
return return
} }
if (isBoolAttr) { if (isBoolAttr) {
elem[isBoolAttr] = !(value === false || value === null) this.element[isBoolAttr] = !(value === false || value === null)
if (elem[isBoolAttr]) { if (this.element[isBoolAttr]) {
elem.setAttribute(attr, '') this.element.setAttribute(this.name, '')
} else { } else {
elem.removeAttribute(attr) this.element.removeAttribute(this.name)
} }
} else {
// value 绑定, 特殊处理
if (attr === 'value') {
elem.value = value
} else { } else {
if (value === null || value === void 0) { if (value === null || value === void 0) {
elem.removeAttribute(attr) this.element.removeAttribute(this.name)
} else { } else {
elem.setAttribute(attr, value) this.element.setAttribute(this.name, value)
}
} }
} }
} }
@ -602,150 +607,87 @@ class PropertyPart extends AttributePart {
this.element[this.name] = value this.element[this.name] = value
} }
} }
// 动画属性
class AnimPart extends AttributePart {
constructor(...args) {
super(...args)
}
commitValue({ type = 'fade', duration, custom, immediate = false } = {}) {
let elem = this.element
let fromto = MODES[type] || MODES.fade
if (custom) {
fromto = custom
}
elem.$animate = function (out = false) {
return animate.call(this, duration, fromto, out)
}
elem.$animate.immediate = immediate
}
}
// 事件属性 // 事件属性
class EventPart extends AttributePart { class EventPart extends AttributePart {
type = EVENT_PART
#listener = null
#prevent = noop
#stop = noop
#checkSelf = noop
constructor(...args) { constructor(...args) {
super(...args) super(...args)
this.type = EVENT_PART
} }
_$setValue(newListener, directiveParent = this) {
newListener =
resolveDirective(this, newListener, directiveParent, 0) || NOTHING
$setValue(listener) { if (newListener === NO_CHANGE) {
let host = this.options.host
let elem = this.element
let name = this.name
let options = {}
let events = host.$events[name] || []
if (this.decorates.length) {
for (let it of this.decorates) {
switch (it) {
case 'stop':
this.#stop = ev => ev.stopPropagation()
break
case 'prevent':
this.#prevent = ev => ev.preventDefault()
break
case 'self':
this.#checkSelf = ev => ev.target === elem
break
case 'capture':
case 'once':
case 'passive':
options[it] = true
break
}
}
}
let shouldRemove = listener !== this.#listener
if (this.#listener) {
for (let i = -1, it; (it = events[++i]); ) {
if (it.el === elem) {
shouldRemove =
shouldRemove ||
options.capture !== it.capture ||
options.once !== it.once ||
options.passive !== it.passive
if (shouldRemove) {
it.el.removeEventListener(name, it.listener, it.options)
events[i] = null
}
break
}
}
}
events = events.filter(it => it !== null)
host.$events[name] = events
if (listener && shouldRemove) {
elem.addEventListener(name, this, options)
this.#listener = listener
events.push({ el: elem, listener: this, options })
}
}
handleEvent(ev) {
this.#stop(ev)
this.#prevent(ev)
if (this.#checkSelf(ev) === false) {
return return
} }
this.#listener.call(this.options.host, ev) let oldListener = this._$committedValue
let shouldRemoveListener =
(newListener === NOTHING && oldListener !== NOTHING) ||
newListener.capture !== oldListener.capture ||
newListener.once !== oldListener.once ||
newListener.passive !== oldListener.passive
let shouldAddListener =
newListener !== NOTHING &&
(oldListener === NOTHING || shouldRemoveListener)
if (shouldRemoveListener) {
this.element.removeEventListener(this.name, this, oldListener)
}
if (shouldAddListener) {
this.element.addEventListener(this.name, this, newListener)
}
this._$committedValue = newListener
}
handleEvent(event) {
if (typeof this._$committedValue === 'function') {
this._$committedValue.call(this.options?.host || this.element, event)
} else {
this._$committedValue.handleEvent(event)
}
} }
} }
class ElementPart { class ElementPart {
type = ELEMENT_PART
constructor(element, parent, options) { constructor(element, parent, options) {
this.element = element this.element = element
this.$parent = parent this.type = ELEMENT_PART
this._$disconnectableChildren = void 0
this._$parent = parent
this.options = options this.options = options
} }
get _$isConnected() {
$setValue() {} return this._$parent._$isConnected
}
_$setValue(value) {
resolveDirective(this, value)
}
} }
export function render(value, container, options = {}) { export function render(value, container, options = {}) {
let part = container[WC_PART] let part = container[WC_PART]
if (part === void 0) { if (part === void 0) {
part = new ChildPart( container[WC_PART] = part = new ChildPart(
container.insertBefore(createMarker(), null), container.insertBefore(createMarker(), null),
null, null,
void 0, void 0,
options options
) )
container[WC_PART] = part
} }
part.$setValue(value, true) part._$setValue(value)
return part return part
} }
export function html(strings, ...values) { export const html = (strings, ...values) => {
return { return {
__dom_type__: HTML_RESULT, __dom_type__: HTML_RESULT,
strings, strings,
values values
} }
} }
export const svg = (strings, ...values) => {
export function raw(str, values = []) {
let strings = values.length ? str.split('%s') : [str]
strings.raw = true
return html(strings, ...values)
}
export function svg(strings, ...values) {
return { return {
__dom_type__: SVG_RESULT, __dom_type__: SVG_RESULT,
strings, strings,

View File

@ -1,5 +1,5 @@
/** /**
* { wkit library } * {wcui的核心库, 基于lit-core二次开发}
* @author yutent<yutent.io@gmail.com> * @author yutent<yutent.io@gmail.com>
* @date 2023/03/07 18:10:43 * @date 2023/03/07 18:10:43
*/ */
@ -7,93 +7,26 @@ import {
fixedValue, fixedValue,
parsePropsDeclaration, parsePropsDeclaration,
boolMap, boolMap,
KEEP_ALIVE,
KEEP_ALIVE_C,
__finalized__, __finalized__,
__render__,
__init__,
__props__, __props__,
__changed_props__, __changed_props__,
__mounted__, __mounted__,
__pending__ __feedback__,
__pending__,
__prop2attr__,
__attr2prop__,
__clear_update__,
__children__,
__updated__,
__in_tree__
} from './constants.js' } from './constants.js'
import { css, adoptStyles } from './css.js' import { css, adoptStyles } from './css.js'
import { render, html, svg, raw } from './html.js' import { render, html, svg } from './html.js'
import { animate, MODES } from './anim.js' import { nextTick, fire, bind, unbind } from './utils.js'
import { nextTick, fire, bind, unbind, hyphen, camelize } from './utils.js' export { $, $$, offset, outsideClick, clearOutsideClick } from './utils.js'
export { html, css, svg, bind, unbind, nextTick }
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 { export class Component extends HTMLElement {
/** /**
@ -104,36 +37,37 @@ export class Component extends HTMLElement {
let list = [] let list = []
this.finalize() this.finalize()
this.parseAnim()
this[__props__].forEach((options, prop) => { this[__props__].forEach((options, prop) => {
list.push(options.attrName) list.push(prop.toLowerCase())
}) })
return list.concat(this.watches || [], KEEP_ALIVE) return list
} }
static parseAnim() { static createProperty(name, options) {
if (this.hasOwnProperty('animation')) { let key = Symbol(name)
let { let descriptor = {
type = 'fade', get() {
duration, return this[key]
custom, },
immediate = false set(value) {
} = this.animation let oldValue = this[key]
let fromto = MODES[type] || MODES.fade value = fixedValue(value, options)
if (custom) { if (oldValue === value) {
fromto = custom return
} }
Object.defineProperty(this.prototype, '$animate', { this[key] = value
value(out) { this.#requestUpdate(name, oldValue)
if (this[__mounted__]) { if (options.observer) {
return animate.call(this, duration, fromto, out) options.observer.call(this, value, oldValue)
} }
},
enumerable: false
} }
})
this.prototype.$animate.immediate = immediate this[__props__].set(name, options)
delete this.animation
} Object.defineProperty(this.prototype, name, descriptor)
} }
// 处理静态声明 // 处理静态声明
@ -147,22 +81,11 @@ export class Component extends HTMLElement {
if (this.hasOwnProperty('props')) { if (this.hasOwnProperty('props')) {
for (let k in this.props) { for (let k in this.props) {
// 保留关键字, 直接跳过
if (k === KEEP_ALIVE || k === KEEP_ALIVE_C) {
continue
}
let options = parsePropsDeclaration(this.props[k]) let options = parsePropsDeclaration(this.props[k])
let attrName = k.toLowerCase() if (boolMap[k] && k !== boolMap[k]) {
k = boolMap[k]
options.attrName = attrName
if (boolMap[attrName]) {
k = boolMap[attrName]
} else {
options.attrName = hyphen(k)
k = camelize(k)
} }
this[__props__].set(k, options) this.createProperty(k, options)
} }
} }
@ -176,106 +99,37 @@ export class Component extends HTMLElement {
} }
} }
keepAlive = false
removed = false
constructor() { constructor() {
super() super()
this[__pending__] = false this[__pending__] = false
this[__mounted__] = false this[__mounted__] = false
this[__in_tree__] = false
// 这里提前定义一次, 是为了在connectedCallback之前, 就已有赋值时报错的bug // 这里提前定义一次, 是为了在connectedCallback之前, 就已有赋值时报错的bug
this[__changed_props__] = new Map() // 记录一次渲染周期内变化的属性 this[__changed_props__] = new Map() // 记录一次渲染周期内变化的属性
this.host = this // this[__init__]()
this.root = this.shadowRoot || this.attachShadow({ mode: 'open' }) this.root = this.shadowRoot || this.attachShadow({ mode: 'open' })
this.root.ownHost = this
Object.defineProperty(this, '$refs', { Object.defineProperty(this, '$refs', {
value: Object.create(null) value: Object.create(null),
}) enumerable: false
Object.defineProperty(this, '$events', {
value: Object.create(null)
}) })
for (let [prop, options] of this.constructor[__props__]) { this.created()
this.#createProperty(prop, options)
// 按W3C规范, 在构造函数内不可设置节点的属性
// 所以这里只记录需要初始化的属性, 在#init()回调中才去修改
this[__changed_props__].set(prop, this[prop])
} }
nextTick(_ => this.created()) [__init__]() {
} // 这里重新赋值一次, 用于清除掉因为 observer 修正产生的变化
this[__changed_props__] = new Map()
#createProperty(name, options) { // 初始化 props
let key = Symbol(name) this.constructor[__props__].forEach((options, prop) => {
let descriptor this[prop] = this[prop] === void 0 ? options.default : this[prop]
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() { // 若无定义props时, 手动执行一次渲染
for (let [prop, value] of this[__changed_props__]) { if (this[__pending__] === false) {
this.#prop2attr(prop, value) this[__pending__] = true
nextTick(_ => this[__updated__]())
} }
// 这里要清除初始化产生的记录
this[__changed_props__].clear()
this.$requestUpdate()
} }
#getPropOptions(name) { #getPropOptions(name) {
@ -283,42 +137,22 @@ export class Component extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
if (this.$animate) { this[__init__]()
this.style.display = 'none'
}
this.removed = false
this.#init()
adoptStyles(this.root, this.constructor.styles) adoptStyles(this.root, this.constructor.styles)
this[__children__]?.setConnected(true)
} }
disconnectedCallback() { disconnectedCallback() {
if (this.keepAlive) { this[__children__]?.setConnected(false)
nextTick(_ => this.deactivated()) this.unmounted()
} else {
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.removed = true
}
nextTick(_ => this.unmounted())
}
} }
// 监听属性变化 // 监听属性变化
attributeChangedCallback(name, old, val) { attributeChangedCallback(name, old, val) {
if (old === val) { if (old === val) {
return return
} }
if (name === KEEP_ALIVE) { this[__attr2prop__](name, val, old)
this.keepAlive = val !== null
return
}
this.#attr2prop(name, val, old)
} }
/** /**
@ -327,12 +161,11 @@ export class Component extends HTMLElement {
* @param name<String> * @param name<String>
* @param value<String|Boolean|Number> * @param value<String|Boolean|Number>
*/ */
#prop2attr(name, value) { [__prop2attr__](name, value) {
let options = this.#getPropOptions(name) || {} let options = this.#getPropOptions(name)
let attrName = options.attrName
if (options.attribute === false || options.type === null) { if (options.attribute === false) {
this.removeAttribute(attrName) this.removeAttribute(name)
return return
} }
@ -340,17 +173,17 @@ export class Component extends HTMLElement {
case Number: case Number:
case String: case String:
if (value === null) { if (value === null) {
this.removeAttribute(attrName) this.removeAttribute(name)
} else { } else {
this.setAttribute(attrName, value) this.setAttribute(name, value)
} }
break break
case Boolean: case Boolean:
if (value === null || value === false) { if (value === null || value === false) {
this.removeAttribute(attrName) this.removeAttribute(name)
} else { } else {
this.setAttribute(attrName, '') this.setAttribute(name, '')
} }
break break
} }
@ -361,97 +194,71 @@ export class Component extends HTMLElement {
* @param name<String> * @param name<String>
* @param value<String|Boolean|Number> * @param value<String|Boolean|Number>
*/ */
#attr2prop(name, value, old) { [__attr2prop__](name, value, old) {
let _name = boolMap[name] let _name = boolMap[name],
let options, propName options
if (_name) { if (_name) {
name = _name name = _name
} }
propName = camelize(name)
options = this.#getPropOptions(propName) options = this.#getPropOptions(name)
this[name] = fixedValue(value, options)
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) { #requestUpdate(name, oldValue) {
if (name !== void 0) {
this[__changed_props__].set(name, this[name]) this[__changed_props__].set(name, this[name])
this.#prop2attr(name, this[name]) this[__prop2attr__](name, this[name])
}
if (this[__pending__] === false) { if (this[__pending__] === false) {
this[__pending__] = true this[__pending__] = true
nextTick(_ => this.#confirmUpdate()) nextTick(_ => this[__updated__]())
} }
} }
// 确认更新到视图 // 确认更新到视图
#confirmUpdate() { [__updated__]() {
if (this[__pending__]) {
let props = this[__changed_props__] let props = this[__changed_props__]
this.#render() this[__render__]()
this.#feedback(props) this[__feedback__](props)
this.#clearUpdate() this[__clear_update__]()
}
} }
// 更新回调反馈 // 更新回调反馈
#feedback(props) { [__feedback__](props) {
if (this[__in_tree__] === false) {
this[__in_tree__] = document.body.contains(this)
}
if (this[__in_tree__]) {
// 初始化时不触发updated回调 // 初始化时不触发updated回调
if (this[__mounted__] === false) { if (this[__mounted__] === false) {
this[__mounted__] = true this[__mounted__] = true
if (this.$animate?.immediate) {
this.$animate()
}
nextTick(_ => {
if (this.keepAlive) {
this.activated()
}
this.mounted() this.mounted()
this.$requestUpdate()
})
} else { } else {
nextTick(_ => this.updated(props)) this.updated(props)
}
} }
} }
#clearUpdate() { [__clear_update__]() {
this[__changed_props__].clear() this[__changed_props__] = new Map()
this[__pending__] = false this[__pending__] = false
} }
// 渲染视图 // 渲染视图
#render() { [__render__]() {
try {
let ast = this.render() let ast = this.render()
render(ast, this.root, {
host: this
})
} catch (err) {
console.error(err)
}
}
this[__children__] = render(ast, this.root, {
host: this,
isConnected: !this[__mounted__] && this.isConnected
})
}
// 几个生命周期回调 // 几个生命周期回调
created() {} created() {}
mounted() {} mounted() {}
activated() {}
deactivated() {}
unmounted() {} unmounted() {}
updated() {} updated() {}
render() { render() {
@ -466,7 +273,7 @@ export class Component extends HTMLElement {
unbind(this, type, callback, options) unbind(this, type, callback, options)
} }
$emit(type, data = {}, stop = false) { $emit(type, data = {}) {
return fire(this, type, data, stop) return fire(this, type, data)
} }
} }

View File

@ -4,18 +4,18 @@
* @date 2023/03/07 22:11:30 * @date 2023/03/07 22:11:30
*/ */
export function noop() {} export function $(selector, container) {
export function $(selector, container, multi) {
let fn = multi ? 'querySelectorAll' : 'querySelector'
if (container) { if (container) {
return container[fn](selector) return container.querySelector(selector)
} }
return document.body[fn](selector) return document.body.querySelector(selector)
} }
export function $$(selector, container) { export function $$(selector, container) {
return $(selector, container, true) if (container) {
return container.querySelectorAll(selector)
}
return document.body.querySelectorAll(selector)
} }
export const nextTick = (function () { export const nextTick = (function () {
@ -24,14 +24,11 @@ export const nextTick = (function () {
let bool = false let bool = false
function callback() { function callback() {
while (queue.length > 0) { let n = queue.length
let fn = queue.shift() for (let i = 0; i < n; i++) {
try { queue[i]()
fn()
} catch (err) {
console.error(err)
}
} }
queue = queue.slice(n)
} }
new MutationObserver(callback).observe(node, { characterData: true }) new MutationObserver(callback).observe(node, { characterData: true })
@ -43,46 +40,6 @@ export const nextTick = (function () {
} }
})() })()
export function range(...args) {
let start = 0,
end = 0,
step = 1,
out = []
switch (args.length) {
case 1:
end = args[0]
break
case 2:
case 3:
;[start, end, step = 1] = args
step = Math.abs(step) || 1
break
}
if (start > end) {
;[start, end] = [end, start]
}
for (let i = start; i < end; i += step) {
out.push(i)
}
return out
}
//驼峰转换为连字符线风格
export function hyphen(target) {
return target.replace(/([a-z\d])([A-Z]+)/g, '$1-$2').toLowerCase()
}
//连字符转换为驼峰风格
export function camelize(target) {
//提前判断,提高效率
if (target.indexOf('-') < 0) {
return target
}
return target.replace(/\-([a-z])/g, (m, s) => s.toUpperCase())
}
//取得距离页面左上角的坐标 //取得距离页面左上角的坐标
export function offset(node) { export function offset(node) {
try { try {
@ -108,20 +65,12 @@ export function offset(node) {
/** /**
* 事件绑定 * 事件绑定
*/ */
export function bind(dom, type = '', selector, fn, phase = false) { export function bind(dom, type, selector, fn, phase = true) {
let events = type.split(',') let events = type.split(',')
let callback let callback
let isWc = dom && dom.host === dom
let host = isWc ? dom : null
if (!dom || !type) {
return console.error(
"Argument Error: function bind's arg 1 must be a document obejct"
)
}
if (typeof selector === 'function') { if (typeof selector === 'function') {
phase = fn || false phase = fn
fn = selector fn = selector
selector = null selector = null
} else { } else {
@ -131,25 +80,13 @@ export function bind(dom, type = '', selector, fn, phase = false) {
fn = fn || noop fn = fn || noop
} }
if (isWc === false) {
let node = dom
while (node && node !== document.body && node !== document) {
if (node.ownHost) {
isWc = true
host = node.ownHost
break
}
node = node.parentNode
}
}
if (selector) { if (selector) {
callback = function (ev) { callback = function (ev) {
let agents = $(selector, dom) let agents = $(selector, ev.currentTarget)
let elem = ev.target let elem = ev.target
if (agents) { if (agents) {
while (true) { while (true) {
if (elem === dom) { if (elem === ev.currentTarget) {
break break
} }
if (agents.contains(elem)) { if (agents.contains(elem)) {
@ -166,25 +103,7 @@ export function bind(dom, type = '', selector, fn, phase = false) {
} }
events.forEach(function (t) { events.forEach(function (t) {
t = t.trim() dom.addEventListener(t.trim(), callback, phase)
if (isWc) {
host.$events[t] ??= []
let _list = host.$events[t]
if (_list.length) {
let idx = _list.findIndex(
it =>
it.el === dom && it.listener === callback && it.options === phase
)
if (idx > -1) {
let item = _list[idx]
_list.splice(idx, 1)
dom.removeEventListener(t, item.listener, item.options)
}
}
_list.push({ el: dom, listener: callback, options: phase })
}
dom.addEventListener(t, callback, phase)
}) })
return callback return callback
} }
@ -192,7 +111,7 @@ export function bind(dom, type = '', selector, fn, phase = false) {
/** /**
* 解除事件绑定 * 解除事件绑定
*/ */
export function unbind(dom, type = '', fn = noop, phase = false) { export function unbind(dom, type, fn = noop, phase = false) {
let events = type.split(',') let events = type.split(',')
events.forEach(function (t) { events.forEach(function (t) {
dom.removeEventListener(t.trim(), fn, phase) dom.removeEventListener(t.trim(), fn, phase)
@ -228,14 +147,9 @@ export function clearOutsideClick(fn = noop) {
unbind(document, 'mousedown', fn) unbind(document, 'mousedown', fn)
} }
/** export function fire(el, name = 'click', data = {}) {
* @param el <DOM> 节点 let ev = document.createEvent('Events')
* @param name <String> 自定义的事件名 ev.initEvent(name, true, true)
* @param data <Object> 要合并进event对象的参数
* @param stop <Boolean> 是否禁止事件冒泡
*/
export function fire(el, name = 'click', data = {}, stop) {
let ev = new Event(name, { bubbles: !stop, cancelable: true })
Object.assign(ev, data) Object.assign(ev, data)
el.dispatchEvent(ev) el.dispatchEvent(ev)
} }

View File

@ -1,47 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<title>wkit test page</title>
<style>
body {line-height: 1.5;font-size:14px;background:var(--page-color);color:var(--text-color);}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
border-radius: 10px;
background: var(--summary-bg-color);
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--border-color-dark);
}
::-webkit-scrollbar-thumb:hover {
background: var(--summary-color);
}
@media screen and (max-width: 480px) {
::-webkit-scrollbar {
display: none;
width: 0;
}
}
</style>
<script type="importmap">
{
"imports":{
"es.shim":"//jscdn.ink/es.shim/latest/index.js",
"wkit":"//127.0.0.1:9999/src/index.js",
"wkitd":"//jscdn.ink/wkitd/1.3.10/index.js",
"fetch":"//jscdn.ink/@bytedo/fetch/latest/next.js",
"@bd/ui/":"//jscdn.ink/@bd/ui/0.1.16/"
}
}
</script>
<script type="module" src="./test.js"></script>
</head>
<body>
<wc-app></wc-app>
</body>
</html>

View File

@ -1,31 +0,0 @@
import { html, Component, live } from 'wkit'
class App extends Component {
foo = ''
bar = { a: 333 }
created() {
// live = live.bind(this)
}
reset() {
this.foo = ''
this.bar.a = '666'
this.$requestUpdate()
}
render() {
return html`
<p>${this.foo}</p>
<p>${this.bar.a}</p>
<input value=${live.bind(this, 'bar.a')} />
<input value=${this.foo} />
<input value=${live.bind(this, 'foo')} />
<button @click=${this.reset}>666</button>
`
}
}
App.reg('app')