Compare commits

...

98 Commits

Author SHA1 Message Date
yutent f70f4a54fb update 2024-06-17 12:03:39 +08:00
yutent dde82e9860 完成双向绑定的逻辑; 更新开源协议声明 2024-06-17 11:32:36 +08:00
yutent fc585abcdf 优化value属性绑定, 自动转为赋值绑定 2024-06-14 18:55:15 +08:00
yutent 0f09245657 增加 when和which方法; 同时mounted回调时更新渲染 2024-06-12 19:02:13 +08:00
yutent 065271f2f6 修复input等有用户输入交互的组件, 因为value属性初值和终值都为空时, 刷新渲染不会清除输入内容的问题 2024-05-17 17:51:31 +08:00
yutent c56013c9b4 优化props解析 2024-05-11 18:44:03 +08:00
yutent db7098c4f8 代码优化精简 2023-12-14 12:36:06 +08:00
yutent ea6010c782 优化模板解析;修复属性代理逻辑;优化事件销毁机制 2023-11-30 11:35:43 +08:00
yutent b4e7f5dfae 优化模板解析;修复事件绑定 2023-11-30 00:17:59 +08:00
yutent bbe04eae83 props增加异常打印 2023-11-23 17:42:00 +08:00
yutent 47090149f8 增加特殊类型null的支持 2023-11-23 10:32:30 +08:00
yutent afac6fddb2 修复属性绑定 2023-11-20 13:59:18 +08:00
yutent 0043895a66 优化动画逻辑 2023-11-20 11:43:11 +08:00
yutent 0be2bf7971 增加selection的样式定义 2023-11-16 17:22:25 +08:00
yutent ff51e09e39 优化在wkitd中使用的逻辑 2023-11-15 17:26:12 +08:00
yutent 0f33586ff4 1.10.10 2023-09-22 18:38:23 +08:00
yutent 12b4417a94 增加容错处理 2023-09-22 16:16:04 +08:00
yutent 4ebf74f40f 精简AST,;增加range 2023-09-21 10:24:53 +08:00
yutent fd154f1bcf rename 2023-09-20 19:11:22 +08:00
yutent 3908bf7fe6 精简模板解析 2023-09-20 19:08:11 +08:00
yutent c5333cbc2a 移除一些无用的API 2023-09-20 19:02:34 +08:00
yutent ce4bf92ce2 created回调改为异步 2023-09-20 17:37:54 +08:00
yutent fb5068b040 优化异步回调的触发 2023-09-20 17:27:15 +08:00
yutent 4f4e8970ca 优化异常处理 2023-09-19 18:35:33 +08:00
yutent f54a5a3653 1.10.7 2023-09-19 11:56:13 +08:00
yutent cf344bcd4b 修复复杂类型属性的监听;调整keepAlive设定 2023-09-19 11:53:10 +08:00
yutent eebe3c63fc 增加静态属性watches的支持 2023-09-18 17:31:12 +08:00
yutent 0483d6efc3 优化属性定义, 支持深层代理 2023-09-04 14:10:57 +08:00
yutent 2aa9ab5af0 render增加容错处理 2023-09-04 11:25:17 +08:00
yutent 87496a1487 修改仓库地址;优化生命周期的触发 2023-09-01 17:51:28 +08:00
yutent 4c3d3eae21 节点被移除时, 同时取消注册到wkitd 2023-08-15 14:36:16 +08:00
yutent 0a70cb1b2e 支持wkitd响应式状态管理 2023-08-15 14:29:00 +08:00
yutent 8c1b1350b0 1.10.2 2023-08-15 11:45:03 +08:00
yutent f53cb98bc4 修复属性定义 2023-08-15 11:44:17 +08:00
yutent c0eb316f87 Merge pull request #2 from bytedo/dev
一小波优化
2023-08-11 19:11:16 +08:00
yutent 079589febb 1.10.1 2023-08-11 19:08:59 +08:00
yutent 6f3ea0f086 增加keepalive支持 2023-08-11 19:08:31 +08:00
yutent 5748e6a760 修改模板缓存逻辑 2023-08-11 17:42:27 +08:00
yutent 289c7142ef 修复动画配置, 增加立刻执行设定; 修复props声明 2023-08-11 14:16:13 +08:00
yutent e9d5add0d0 Merge pull request #1 from bytedo/dev
调整props定义, 复杂类型改用Proxy
2023-08-11 10:20:03 +08:00
yutent 9227233d7a 调整props定义, 复杂类型改用Proxy 2023-08-11 10:18:50 +08:00
yutent aca6d75553 修复props简写定义 2023-08-10 15:05:52 +08:00
yutent f9e766ab4a 增加非显式属性和快速定义 2023-08-07 18:56:41 +08:00
yutent da056ae47f 1.9.7 2023-07-19 12:27:19 +08:00
yutent 408ba6ca2f 微调adoptedStyleSheets的赋值, 兼容国产辣鸡浏览器 2023-05-29 10:03:33 +08:00
yutent 1a12b21cc2 fixed merged 2023-05-25 15:35:32 +08:00
yutent 3256999b09 Merge branch 'master' of github.com:bytedo/wkit 2023-05-25 15:34:54 +08:00
yutent 05ec00658c 项目更名 2023-05-25 15:33:31 +08:00
yutent 69fae35b2f Update Readme.md 2023-05-10 17:55:54 +08:00
yutent 5ddb00e6b6 修改事件方法为现代语法 2023-05-09 14:08:31 +08:00
yutent da5e5fd4a4 修复props定义 2023-05-08 19:17:55 +08:00
yutent ecd46ba23d $emit()支持禁止事件冒泡 2023-05-08 18:43:04 +08:00
yutent 134e465dd0 优化props定义 2023-05-06 11:59:58 +08:00
yutent 5e1a5d7ebd 修复在iframe中运行时, 调用document.open()报错的问题 2023-04-26 19:09:46 +08:00
yutent 714d9ceb3b 增加raw方法 2023-04-21 15:26:27 +08:00
yutent a456df0b53 更新readme 2023-04-17 16:39:45 +08:00
yutent c9b21cd790 重构事件系统; 事件增加修饰符支持 2023-04-17 16:13:59 +08:00
yutent 5326af856c 修复事件绑定在影子节点上时无法自动销毁的bug 2023-04-13 18:27:37 +08:00
yutent 948c4e08c6 fixed 2023-04-07 16:29:54 +08:00
yutent 7adf718231 修复stypeMap传值为0的bug 2023-04-07 11:25:34 +08:00
yutent 878e469a1a 更新readme 2023-04-07 10:11:58 +08:00
yutent bd36c96292 更新readme 2023-04-07 10:10:17 +08:00
yutent 87c79f56ec 动画结束后自动清除 2023-04-06 11:50:26 +08:00
yutent 7ee22886cb 优化动画, 增加2种内置动画 2023-04-04 14:46:50 +08:00
yutent 0ee7ddc6a1 动画方法返回Promise对象 2023-04-04 13:55:01 +08:00
yutent 724b3732a0 优化动画封装 2023-04-04 12:04:17 +08:00
yutent d7918768b9 更新关键词 2023-04-03 10:50:18 +08:00
yutent b36bb36fda 暴露fire方法 2023-03-29 17:39:00 +08:00
yutent 363a2ce623 增加动画的支持, 并内置4种动画 2023-03-29 12:08:37 +08:00
yutent 91ba64722e 增加基础动画的支持 2023-03-28 19:35:47 +08:00
yutent 21a565e04c 更新文档 2023-03-27 19:03:19 +08:00
yutent 8c033984d4 1.7.1 2023-03-27 18:56:14 +08:00
yutent f8f749d32e 修复设置attribute为false时,初始值不生效的bug 2023-03-27 18:55:47 +08:00
yutent 8770c32e5b 增加classMap和styleMap2个函数 2023-03-27 16:13:46 +08:00
yutent 7568e701cf 增加事件销毁机制 2023-03-24 16:45:39 +08:00
yutent f7e0ce08c3 优化代码,减少一些不必须的symbol 2023-03-24 11:29:39 +08:00
yutent d2ccaa5c3d 修复noop未定义的bug 2023-03-23 19:42:46 +08:00
yutent 9641f5b82a 修复ref解析部分缺失的bug 2023-03-23 11:50:28 +08:00
yutent 9b76cfa141 修复组件内组件的mounted不触发的bug 2023-03-23 11:17:55 +08:00
yutent f397c57c37 fixed 2023-03-22 18:56:14 +08:00
yutent c416622f72 修复ref解析丢失的bug 2023-03-22 18:49:46 +08:00
yutent 7669b3b713 修复因兼容vue二次渲染引起的bug 2023-03-21 23:06:40 +08:00
yutent 81f8fd45eb 调整渲染逻辑 2023-03-21 19:48:53 +08:00
yutent 886829f44d 增加默认render; 优化渲染机制,避免在vue下被渲染2次 2023-03-21 18:23:13 +08:00
yutent 310e2bff1e 合并脚本命令 2023-03-21 16:46:53 +08:00
yutent 836d566f55 内置注册方法 2023-03-21 16:45:10 +08:00
yutent 439287da9c 调整初始化机制, 以兼容vue 2023-03-21 15:43:41 +08:00
yutent 406f581ecf 修复布尔属性的attribute赋值 2023-03-21 09:48:29 +08:00
yutent 0eecaa22dc 修复没有动态绑定时, ref不生效的bug 2023-03-20 11:29:19 +08:00
yutent 58d4277964 调整attribute定义 2023-03-20 10:43:17 +08:00
yutent 6ca5045bd0 1.5.2 2023-03-17 19:02:32 +08:00
yutent 304d320cb3 修复一处继承调用的bug 2023-03-17 19:02:20 +08:00
yutent d0af1270dc 修复ref标识的解析 2023-03-17 10:59:48 +08:00
yutent 8cebdd1994 增加节点ref标识的支持 2023-03-17 10:18:40 +08:00
yutent 195f3b1828 调整传参 2023-03-16 23:42:04 +08:00
yutent a6efaaabf0 修复observer导致内存溢出的bug 2023-03-16 16:31:16 +08:00
yutent 3bc02e2e13 优化组件的初始化 2023-03-16 14:32:35 +08:00
yutent a57016e14e 精简一波模板解析;优化布尔属性的处理;支持赋值属性的支持 2023-03-16 13:40:25 +08:00
12 changed files with 1113 additions and 636 deletions

5
.gitignore vendored
View File

@ -1,12 +1,9 @@
*.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

1
.npmignore Normal file
View File

@ -0,0 +1 @@
test/

View File

@ -1,12 +1,30 @@
## 9号UI组件库的核心 ## Wkit
> 一个简单易用、功能完善的用于开发`web components`的轻量级开发库。模板解析基于`lit-html`二次开发
![downloads](https://img.shields.io/npm/dt/wkit.svg)
![version](https://img.shields.io/npm/v/wkit.svg)
### 开发文档 ### 开发文档
[开发文档](https://github.com/bd-js/core/wiki) [开发文档](https://git.wkit.fun/bytedo/wkit/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
import { css, html, Component } from '//jscdn.ink/@bd/core/latest/index.js' // alias wkit='//jscdn.ink/wkit/latest/index.js'
import { css, html, Component } from 'wkit'
class Hello extends Component { class Hello extends Component {
@ -20,7 +38,7 @@ class Hello extends Component {
bar: String // 简写 bar: String // 简写
} }
// 若需要支持 scss, 则需要使用 @bd/wcui-cli,预处理 // 若需要支持 scss语法, 则需要使用构建工具编译
// 可支持数组 // 可支持数组
static styles = css` static styles = css`
button { color: #09f; } button { color: #09f; }
@ -48,7 +66,12 @@ class Hello extends Component {
} }
if (!customElements.get('wc-hello')) {
customElements.define('wc-hello', Hello) customElements.define('wc-hello', Hello)
}
// 和上面的写法等价
// 如果不希望修改前缀`wc-`的话, 可以直接调用内置的reg方法注册组件即可。
Hello.reg('hello')
/* /*
@ -62,3 +85,7 @@ customElements.define('wc-hello', Hello)
### 开源协议
- BSD 3-Clause License (Lit框架的html模板解析的源码)
- MIT (除Lit代码之外的所有代码)

View File

@ -1,26 +1,29 @@
{ {
"name": "@bd/core", "name": "wkit",
"version": "1.3.0", "version": "1.12.0",
"type": "module", "type": "module",
"description": "百搭UI组件库的核心", "description": "A library for building fast, lightweight web components.",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [
"dist/*" "dist/*"
], ],
"scripts": { "scripts": {
"build": "esbuild src/index.js --minify --bundle --format=esm --target=esnext --outfile=dist/index.js", "build:next": "esbuild src/index.js --minify --bundle --format=esm --target=esnext --outfile=dist/index.js",
"build:es6": "esbuild src/index.js --minify --bundle --format=esm --target=es6 --outfile=dist/index.es6.js" "build:es6": "esbuild src/index.js --minify --bundle --format=esm --target=es6 --outfile=dist/index.es6.js",
"build": "npm run build:next && npm run build:es6"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/bd-js/core.git" "url": "https://git.wkit.fun/bytedo/wkit.git"
}, },
"keywords": [ "keywords": [
"bd.js", "wkit",
"bdjs",
"bd-ui",
"wcui", "wcui",
"bd-core" "web-components",
"native",
"cross-framework",
"html",
"css"
], ],
"devDependencies": { "devDependencies": {
"esbuild": "^0.17.11" "esbuild": "^0.17.11"

48
src/anim.js Normal file
View File

@ -0,0 +1,48 @@
/**
* {}
* @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

@ -27,31 +27,25 @@ const boolMap = Object.create(null)
'noShade', 'noShade',
'open', 'open',
'readOnly', 'readOnly',
'selected' 'selected',
'loading'
].forEach(function (name) { ].forEach(function (name) {
boolMap[name.toLowerCase()] = name boolMap[name.toLowerCase()] = name
}) })
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 RESET_CSS_STYLE = `* {box-sizing: border-box;margin: 0;padding: 0;}::before,::after {box-sizing: border-box;}` 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);}`
function getDefaultValue(type) { function getDefaultValue(type) {
switch (type) { switch (type) {
@ -85,8 +79,26 @@ 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: String, default: v + '' } return { type: null, default: '' }
} }
} }
@ -116,6 +128,10 @@ 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
@ -123,7 +139,9 @@ 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 = true options.attribute = options.hasOwnProperty('attribute')
? options.attribute
: true
return options return options
} }
@ -131,11 +149,9 @@ export function fixedValue(value, options) {
switch (options.type) { switch (options.type) {
case Number: case Number:
return value === null ? null : +value || 0 return value === null ? null : +value || 0
break
case Boolean: case Boolean:
return value === false ? false : value !== null return value === false ? false : value !== null
break
case Object: case Object:
if (typeof value === 'object') { if (typeof value === 'object') {
@ -147,7 +163,6 @@ export function fixedValue(value, options) {
return {} return {}
} }
} }
break
case Array: case Array:
if (typeof value === 'object') { if (typeof value === 'object') {
@ -159,10 +174,11 @@ export function fixedValue(value, options) {
return [] return []
} }
} }
break
case null:
return value
default: default:
return value === null ? null : value + '' return value === null || value === void 0 ? null : value + ''
break
} }
} }

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.push(sheet) root.adoptedStyleSheets = [sheet]
} else { } else {
root.appendChild(sheet.elem) root.appendChild(sheet.elem)
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/** /**
* {wcui的核心库, 基于lit-core二次开发} * { wkit library }
* @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,25 +7,93 @@ import {
fixedValue, fixedValue,
parsePropsDeclaration, parsePropsDeclaration,
boolMap, boolMap,
KEEP_ALIVE,
KEEP_ALIVE_C,
__finalized__, __finalized__,
__render__,
__init__,
__props__, __props__,
__changed_props__, __changed_props__,
__mounted__, __mounted__,
__feedback__, __pending__
__pending__,
__prop2attr__,
__attr2prop__,
__clear_update__,
__children__,
__updated__
} from './constants.js' } from './constants.js'
import { css, adoptStyles } from './css.js' import { css, adoptStyles } from './css.js'
import { render, html, svg } from './html.js' import { render, html, svg, raw } from './html.js'
import { nextTick, fire, bind, unbind } from './utils.js' import { animate, MODES } from './anim.js'
export { $, $$, offset, outsideClick, clearOutsideClick } from './utils.js' import { nextTick, fire, bind, unbind, hyphen, camelize } 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 {
/** /**
@ -36,36 +104,36 @@ 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) => {
options.attribute && list.push(prop.toLowerCase()) list.push(options.attrName)
}) })
return list return list.concat(this.watches || [], KEEP_ALIVE)
} }
static createProperty(name, options) { static parseAnim() {
let key = Symbol(name) if (this.hasOwnProperty('animation')) {
let descriptor = { let {
get() { type = 'fade',
return this[key] duration,
}, custom,
set(value) { immediate = false
let oldValue = this[key] } = this.animation
value = fixedValue(value, options) let fromto = MODES[type] || MODES.fade
if (oldValue === value) { if (custom) {
return fromto = custom
} }
this[key] = value Object.defineProperty(this.prototype, '$animate', {
if (options.observer) { value(out) {
options.observer.call(this, value, oldValue) if (this[__mounted__]) {
return animate.call(this, duration, fromto, out)
} }
this.#requestUpdate(name, oldValue)
},
enumerable: false
} }
})
this[__props__].set(name, options) this.prototype.$animate.immediate = immediate
Object.defineProperty(this.prototype, name, descriptor) delete this.animation
}
} }
// 处理静态声明 // 处理静态声明
@ -79,38 +147,135 @@ 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) {
let options = parsePropsDeclaration(this.props[k]) // 保留关键字, 直接跳过
if (boolMap[k] && k !== boolMap[k]) { if (k === KEEP_ALIVE || k === KEEP_ALIVE_C) {
k = boolMap[k] continue
} }
this.createProperty(k, options) 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 delete this.props
} }
static reg(name = '') {
name = 'wc-' + name
if (!customElements.get(name)) {
customElements.define(name, this)
}
}
keepAlive = false
removed = false
constructor() { constructor() {
super() super()
this[__pending__] = false this[__pending__] = false
this[__mounted__] = false this[__mounted__] = false
this[__init__]() // 这里提前定义一次, 是为了在connectedCallback之前, 就已有赋值时报错的bug
this.created() this[__changed_props__] = new Map() // 记录一次渲染周期内变化的属性
} this.host = this
[__init__]() {
this.root = this.shadowRoot || this.attachShadow({ mode: 'open' }) this.root = this.shadowRoot || this.attachShadow({ mode: 'open' })
this.root.ownHost = this
this[__changed_props__] = new Map() // 记录本次变化的属性 Object.defineProperty(this, '$refs', {
// 初始化 props value: Object.create(null)
this.constructor[__props__].forEach((options, prop) => {
this[prop] = options.default
}) })
// 若无定义props时, 手动执行一次渲染 Object.defineProperty(this, '$events', {
if (this[__pending__] === false) { value: Object.create(null)
this[__pending__] = true })
nextTick(_ => this[__updated__]())
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) { #getPropOptions(name) {
@ -118,21 +283,42 @@ export class Component extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
if (this.$animate) {
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() {
this[__children__]?.setConnected(false) if (this.keepAlive) {
this.unmounted() nextTick(_ => this.deactivated())
} 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
} }
this[__attr2prop__](name, val) if (name === KEEP_ALIVE) {
this.keepAlive = val !== null
return
}
this.#attr2prop(name, val, old)
} }
/** /**
@ -141,10 +327,12 @@ 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) { if (options.attribute === false || options.type === null) {
this.removeAttribute(attrName)
return return
} }
@ -152,17 +340,17 @@ export class Component extends HTMLElement {
case Number: case Number:
case String: case String:
if (value === null) { if (value === null) {
this.removeAttribute(name) this.removeAttribute(attrName)
} else { } else {
this.setAttribute(name, value) this.setAttribute(attrName, value)
} }
break break
case Boolean: case Boolean:
if (value === null || value === false) { if (value === null || value === false) {
this.removeAttribute(name) this.removeAttribute(attrName)
} else { } else {
this.setAttribute(name, '') this.setAttribute(attrName, '')
} }
break break
} }
@ -173,77 +361,112 @@ export class Component extends HTMLElement {
* @param name<String> * @param name<String>
* @param value<String|Boolean|Number> * @param value<String|Boolean|Number>
*/ */
[__attr2prop__](name, value) { #attr2prop(name, value, old) {
let options = this.#getPropOptions(name) let _name = boolMap[name]
let options, propName
this[name] = fixedValue(value, options) 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, oldValue) { $requestUpdate(name) {
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[__updated__]()) nextTick(_ => this.#confirmUpdate())
} }
} }
// 确认更新到视图 // 确认更新到视图
[__updated__]() { #confirmUpdate() {
if (this[__pending__]) {
try {
let props = this[__changed_props__] let props = this[__changed_props__]
this[__render__]() this.#render()
this[__feedback__](props) this.#feedback(props)
} catch (err) { this.#clearUpdate()
console.error(err)
}
this[__clear_update__]()
}
} }
// 更新回调反馈 // 更新回调反馈
[__feedback__](props) { #feedback(props) {
// 初始化时不触发updated回调 // 初始化时不触发updated回调
if (!this[__mounted__]) { 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 {
this.updated(props) nextTick(_ => this.updated(props))
} }
} }
[__clear_update__]() { #clearUpdate() {
this[__changed_props__] = new Map() this[__changed_props__].clear()
this[__pending__] = false this[__pending__] = false
} }
// 渲染视图 // 渲染视图
[__render__]() { #render() {
let htmlText = this.render() try {
let ast = this.render()
this[__children__] = render(htmlText, this.root, { render(ast, this.root, {
host: this, host: this
isConnected: !this[__mounted__] && this.isConnected
}) })
} catch (err) {
console.error(err)
} }
}
// 几个生命周期回调 // 几个生命周期回调
created() {} created() {}
mounted() {} mounted() {}
activated() {}
deactivated() {}
unmounted() {} unmounted() {}
updated() {} updated() {}
render() {
$on(type, callback) { return html`<slot></slot>`
return bind(this, type, callback)
} }
$off(type, callback) { $on(type, callback, options) {
unbind(this, type, callback) return bind(this, type, callback, options)
} }
$emit(type, data = {}) { $off(type, callback, options) {
return fire(this, type, data) unbind(this, type, callback, options)
}
$emit(type, data = {}, stop = false) {
return fire(this, type, data, stop)
} }
} }

View File

@ -4,18 +4,18 @@
* @date 2023/03/07 22:11:30 * @date 2023/03/07 22:11:30
*/ */
export function $(selector, container) { export function noop() {}
export function $(selector, container, multi) {
let fn = multi ? 'querySelectorAll' : 'querySelector'
if (container) { if (container) {
return container.querySelector(selector) return container[fn](selector)
} }
return document.body.querySelector(selector) return document.body[fn](selector)
} }
export function $$(selector, container) { export function $$(selector, container) {
if (container) { return $(selector, container, true)
return container.querySelectorsAll(selector)
}
return document.body.querySelectorsAll(selector)
} }
export const nextTick = (function () { export const nextTick = (function () {
@ -24,11 +24,14 @@ export const nextTick = (function () {
let bool = false let bool = false
function callback() { function callback() {
let n = queue.length while (queue.length > 0) {
for (let i = 0; i < n; i++) { let fn = queue.shift()
queue[i]() try {
fn()
} catch (err) {
console.error(err)
}
} }
queue = queue.slice(n)
} }
new MutationObserver(callback).observe(node, { characterData: true }) new MutationObserver(callback).observe(node, { characterData: true })
@ -40,6 +43,46 @@ 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 {
@ -65,12 +108,20 @@ export function offset(node) {
/** /**
* 事件绑定 * 事件绑定
*/ */
export function bind(dom, type, selector, fn, phase = true) { export function bind(dom, type = '', selector, fn, phase = false) {
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 phase = fn || false
fn = selector fn = selector
selector = null selector = null
} else { } else {
@ -80,13 +131,25 @@ export function bind(dom, type, selector, fn, phase = true) {
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, ev.currentTarget) let agents = $(selector, dom)
let elem = ev.target let elem = ev.target
if (agents) { if (agents) {
while (true) { while (true) {
if (elem === ev.currentTarget) { if (elem === dom) {
break break
} }
if (agents.contains(elem)) { if (agents.contains(elem)) {
@ -103,7 +166,25 @@ export function bind(dom, type, selector, fn, phase = true) {
} }
events.forEach(function (t) { events.forEach(function (t) {
dom.addEventListener(t.trim(), callback, phase) t = t.trim()
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
} }
@ -111,7 +192,7 @@ export function bind(dom, type, selector, fn, phase = true) {
/** /**
* 解除事件绑定 * 解除事件绑定
*/ */
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)
@ -147,9 +228,14 @@ export function clearOutsideClick(fn = noop) {
unbind(document, 'mousedown', fn) unbind(document, 'mousedown', fn)
} }
export function fire(el, name = 'click', data = {}) { /**
let ev = document.createEvent('Events') * @param el <DOM> 节点
ev.initEvent(name, true, true) * @param name <String> 自定义的事件名
* @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)
} }

47
test/index.html Normal file
View File

@ -0,0 +1,47 @@
<!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>

31
test/test.js Normal file
View File

@ -0,0 +1,31 @@
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')