Compare commits

...

13 Commits

11 changed files with 369 additions and 193 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,5 +1,5 @@
## Wkit ## Wkit
> A library for building fast, lightweight web components. > 一个简单易用、功能完善的用于开发`web components`的轻量级开发库。模板解析基于`lit-html`二次开发
![downloads](https://img.shields.io/npm/dt/wkit.svg) ![downloads](https://img.shields.io/npm/dt/wkit.svg)
![version](https://img.shields.io/npm/v/wkit.svg) ![version](https://img.shields.io/npm/v/wkit.svg)
@ -15,6 +15,7 @@
- `:xxx=yyy`, 属性绑定, 类似`vue`,在属性前加上`:`, 该属性不再使用`setAttribute()`, 而是直接赋值, 可满足你需要传递复杂数据结构的需求。 - `:xxx=yyy`, 属性绑定, 类似`vue`,在属性前加上`:`, 该属性不再使用`setAttribute()`, 而是直接赋值, 可满足你需要传递复杂数据结构的需求。
- `#animation={type}`, 开箱即用的动画配置, 内置6种动画`fade(默认), scale, bounce, micro-bounce, rotate, slide` - `#animation={type}`, 开箱即用的动画配置, 内置6种动画`fade(默认), scale, bounce, micro-bounce, rotate, slide`
- `ref=xxx`, 类似`vue`的节点标识, 可方便的在`mounted()`之后通过 `this.$refs.xxx` 来访问该节点 - `ref=xxx`, 类似`vue`的节点标识, 可方便的在`mounted()`之后通过 `this.$refs.xxx` 来访问该节点
- 内置特色的双向绑定方法`live()`
- 用内置的`bind()`方法来给当前组件绑定事件时, 组件移除时事件也会自动销毁,无需手动销毁。 - 用内置的`bind()`方法来给当前组件绑定事件时, 组件移除时事件也会自动销毁,无需手动销毁。
- 灵活的`props`定义 - 灵活的`props`定义
@ -84,3 +85,7 @@ Hello.reg('hello')
### 开源协议
- BSD 3-Clause License (Lit框架的html模板解析的源码)
- MIT (除Lit代码之外的所有代码)

View File

@ -1,6 +1,6 @@
{ {
"name": "wkit", "name": "wkit",
"version": "1.10.12", "version": "1.12.0",
"type": "module", "type": "module",
"description": "A library for building fast, lightweight web components.", "description": "A library for building fast, lightweight web components.",
"main": "dist/index.js", "main": "dist/index.js",
@ -14,7 +14,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://git.wkit.fun/bytedo/wkit.git" "url": "https://git.wkit.fun/bytedo/wkit.git"
}, },
"keywords": [ "keywords": [
"wkit", "wkit",

View File

@ -5,7 +5,7 @@
*/ */
export function animate(duration = 200, fromto = [], out = false) { export function animate(duration = 200, fromto = [], out = false) {
if (out === false) { if (out === false && this.style.display === 'none') {
this.style.display = '' this.style.display = ''
} }
let res = this.animate(fromto, { let res = this.animate(fromto, {

View File

@ -44,7 +44,6 @@ 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 __pending__ = Symbol('pending') export const __pending__ = Symbol('pending')
export const __children__ = Symbol('children')
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;}::selection {background: var(--selection-background, var(--color-plain-a));color: var(--selection-color, inherit);}`
@ -84,16 +83,14 @@ function getType(v) {
let type = String let type = String
let attribute = true let attribute = true
if (v.includes('!')) { if (v.includes('!')) {
v = v.split('!') if (v.startsWith('str!')) {
let _t = v.shift() v = v.slice(4)
if (_t === 'str') { } else if (v.startsWith('num!')) {
v = v.join('!')
} else if (_t === 'num') {
type = Number type = Number
v = +v.shift() || 0 v = +v.slice(4) || 0
} else if (_t === 'bool') { } else if (v.startsWith('bool!')) {
type = Boolean type = Boolean
v = v.shift() v = v.slice(5)
v = v !== 'false' && v !== '' v = v !== 'false' && v !== ''
} }
attribute = false attribute = false
@ -101,7 +98,7 @@ function getType(v) {
return { type, default: v, attribute } return { type, default: v, attribute }
default: default:
return { type: String, default: v + '' } return { type: null, default: '' }
} }
} }
@ -131,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
@ -174,6 +175,9 @@ 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

@ -6,30 +6,30 @@
import { boolMap, WC_PART, NOTHING } from './constants.js' import { boolMap, WC_PART, NOTHING } from './constants.js'
import { animate, MODES } from './anim.js' import { animate, MODES } from './anim.js'
import { nextTick, noop } from './utils.js'
const boundAttributeSuffix = '$wc$' const BIND_ATTR_SUFFIX = '{{$wkit$}}'
const marker = `wc$${String(Math.random()).slice(9)}$` const MARKER = `{{^wkit${String(Math.random()).slice(-8)}^}}`
const markerMatch = '?' + marker const MARKER_MATCH = '?' + MARKER
const nodeMarker = `<${markerMatch}>` const NODE_MARKER = `<${MARKER_MATCH}>`
const createMarker = (v = '') => document.createComment(v)
// 是否原始值
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 =>
isArray(value) || value ? isArray(value) || typeof value[Symbol.iterator] === 'function' : false
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 textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g const TEXT_END_REGEX = /<(?:(!--|\/[^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 commentEndRegex = /-->/g const COMMENT_END_REGEXP = /-->/g
const comment2EndRegex = />/g const COMMENT_END_REGEXP_2 = />/g
const tagEndRegex = new RegExp( const TAG_END_REGEXP = 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 +37,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 singleQuoteAttrEndRegex = /'/g const SINGLE_QUOTE_REGEXP = /'/g
const doubleQuoteAttrEndRegex = /"/g const DOUBLE_QUOTE_REGEXP = /"/g
const rawTextElement = /^(?:script|style|textarea|title)$/i const RAW_TEXT_ELEM_REGEXP = /^(?: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
@ -51,14 +51,12 @@ const COMMENT_PART = 7
const TEMPLATE_CACHE = new Map() const TEMPLATE_CACHE = new Map()
const walker = document.createTreeWalker(document, 129, null, false) const walker = document.createTreeWalker(document, 129, null, false)
function noop() {}
function getTemplateHtml(strings, type) { function getTemplateHtml(strings, type) {
let len = strings.length - 1 let len = strings.length - 1
let attrNames = [] let attrNames = []
let html2 = type === SVG_RESULT ? '<svg>' : '' let html2 = type === SVG_RESULT ? '<svg>' : ''
let rawTextEndRegex let rawTextEndRegex
let regex = textEndRegex let regex = TEXT_END_REGEX
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
@ -72,25 +70,26 @@ 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 = commentEndRegex regex = COMMENT_END_REGEXP
} else if (match[COMMENT_START] !== void 0) { } else if (match[COMMENT_START] !== void 0) {
regex = comment2EndRegex regex = COMMENT_END_REGEXP_2
} else if (match[TAG_NAME] !== void 0) { } else if (match[TAG_NAME] !== void 0) {
if (rawTextElement.test(match[TAG_NAME])) { if (RAW_TEXT_ELEM_REGEXP.test(match[TAG_NAME])) {
rawTextEndRegex = new RegExp(`</${match[TAG_NAME]}`, 'g') rawTextEndRegex = new RegExp(`</${match[TAG_NAME]}`, 'g')
} }
regex = tagEndRegex regex = TAG_END_REGEXP
} else if (match[DYNAMIC_TAG_NAME] !== void 0) { } else if (match[DYNAMIC_TAG_NAME] !== void 0) {
regex = tagEndRegex regex = TAG_END_REGEXP
} }
} else if (regex === tagEndRegex) { } else if (regex === TAG_END_REGEXP) {
if (match[ENTIRE_MATCH] === '>') { if (match[ENTIRE_MATCH] === '>') {
regex = regex =
rawTextEndRegex !== null && rawTextEndRegex !== void 0 rawTextEndRegex !== null && rawTextEndRegex !== void 0
? rawTextEndRegex ? rawTextEndRegex
: textEndRegex : TEXT_END_REGEX
attrNameEndIndex = -1 attrNameEndIndex = -1
} else if (match[ATTRIBUTE_NAME] === void 0) { } else if (match[ATTRIBUTE_NAME] === void 0) {
attrNameEndIndex = -2 attrNameEndIndex = -2
@ -99,58 +98,76 @@ function getTemplateHtml(strings, type) {
attrName = match[ATTRIBUTE_NAME] attrName = match[ATTRIBUTE_NAME]
regex = regex =
match[QUOTE_CHAR] === void 0 match[QUOTE_CHAR] === void 0
? tagEndRegex ? TAG_END_REGEXP
: match[QUOTE_CHAR] === '"' : match[QUOTE_CHAR] === '"'
? doubleQuoteAttrEndRegex ? DOUBLE_QUOTE_REGEXP
: singleQuoteAttrEndRegex : SINGLE_QUOTE_REGEXP
} }
} else if ( } else if (
regex === doubleQuoteAttrEndRegex || regex === DOUBLE_QUOTE_REGEXP ||
regex === singleQuoteAttrEndRegex regex === SINGLE_QUOTE_REGEXP
) { ) {
regex = tagEndRegex regex = TAG_END_REGEXP
} else if (regex === commentEndRegex || regex === comment2EndRegex) { } else if (
regex = textEndRegex regex === COMMENT_END_REGEXP ||
regex === COMMENT_END_REGEXP_2
) {
regex = TEXT_END_REGEX
} else { } else {
regex = tagEndRegex regex = TAG_END_REGEXP
rawTextEndRegex = void 0 rawTextEndRegex = void 0
} }
} }
let end = let end =
regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : '' regex === TAG_END_REGEXP && strings[i + 1].startsWith('/>') ? ' ' : ''
html2 += html2 +=
regex === textEndRegex regex === TEXT_END_REGEX
? s + nodeMarker ? s + NODE_MARKER
: attrNameEndIndex >= 0 : attrNameEndIndex >= 0
? (attrNames.push(attrName), ? (attrNames.push(attrName),
s.slice(0, attrNameEndIndex) + s.slice(0, attrNameEndIndex) +
boundAttributeSuffix + BIND_ATTR_SUFFIX +
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 (!Array.isArray(strings) || !strings.hasOwnProperty('raw')) { if (!isArray(strings) || !strings.hasOwnProperty('raw')) {
throw new Error('invalid html ast') throw new Error('invalid html ast')
} }
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 {
constructor({ strings, ['__dom_type__']: type }, options) { parts = []
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
@ -164,17 +181,14 @@ class Template {
let attrsToRemove = [] let attrsToRemove = []
for (let name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
if ( if (name.endsWith(BIND_ATTR_SUFFIX) || name.startsWith(MARKER)) {
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() + boundAttributeSuffix realName.toLowerCase() + BIND_ATTR_SUFFIX
) )
let statics = value.split(marker) let statics = value.split(MARKER)
let m = /([#:@])?(.*)/.exec(realName) let m = /([#:@])?(.*)/.exec(realName)
let decorates = [] let decorates = []
@ -210,8 +224,8 @@ class Template {
node.removeAttribute(name) node.removeAttribute(name)
} }
} }
if (rawTextElement.test(node.tagName)) { if (RAW_TEXT_ELEM_REGEXP.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 = ''
@ -225,24 +239,19 @@ class Template {
} }
} else if (node.nodeType === 8) { } else if (node.nodeType === 8) {
let data = node.data let data = node.data
if (data === markerMatch) { if (data === MARKER_MATCH) {
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
}
} }
class TemplateInstance { class TemplateInstance {
@ -264,7 +273,7 @@ class TemplateInstance {
} while ((node = walker.nextNode()) !== null) } while ((node = walker.nextNode()) !== null)
} }
_clone(options) { clone(options) {
let { let {
el: { content }, el: { content },
parts parts
@ -325,7 +334,8 @@ class TemplateInstance {
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) {
@ -342,9 +352,10 @@ class TemplateInstance {
} }
class ChildPart { class ChildPart {
type = CHILD_PART
#value = NOTHING
constructor(startNode, endNode, parent, options) { constructor(startNode, endNode, parent, options) {
this.type = CHILD_PART
this._$committedValue = NOTHING
this.startNode = startNode this.startNode = startNode
this.endNode = endNode this.endNode = endNode
this.$parent = parent this.$parent = parent
@ -360,91 +371,97 @@ class ChildPart {
return parentNode return parentNode
} }
$setValue(value) { $setValue(value, clear) {
if (isPrimitive(value)) { if (isPrimitive(value)) {
if (value === NOTHING || value == null || value === '') { if (value === NOTHING || value == null || value === '') {
if (this._$committedValue !== NOTHING) { if (this.#value !== NOTHING) {
this.#clear() this.#clear()
} }
this._$committedValue = NOTHING this.#value = NOTHING
} else if (value !== this._$committedValue) { } else if (value !== this.#value) {
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, target = this.endNode) {
#insert(node, target = this.endNode) {
return this.startNode.parentNode.insertBefore(node, target) return this.startNode.parentNode.insertBefore(node, target)
} }
_commitNode(value) {
if (this._$committedValue !== value) { #commitNode(value) {
if (this.#value !== value) {
this.#clear() this.#clear()
this._$committedValue = this._insert(value) this.#value = this.#insert(value)
} }
} }
_commitText(value) {
if ( #commitText(value) {
this._$committedValue !== NOTHING && if (this.#value !== NOTHING && isPrimitive(this.#value)) {
isPrimitive(this._$committedValue)
) {
let node = this.startNode.nextSibling let node = this.startNode.nextSibling
node.data = value node.data = value
} else { } else {
this._commitNode(document.createTextNode(value)) this.#commitNode(document.createTextNode(value))
} }
this._$committedValue = value this.#value = value
} }
_commitTemplateResult(result) {
let { values, ['__dom_type__']: type } = result #commitTemplateResult(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 = Template.createElement(type.h)), : (type.el === void 0 && (type.el = createElement(type.h)), type)
type)
if (this._$committedValue?.$template === template) { if (this.#value?.$template === template) {
this._$committedValue._update(values) this.#value.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._$committedValue = instance this.#value = instance
} }
} }
#getTemplate(result) { #getTemplate(result) {
let template = TEMPLATE_CACHE.get(result.strings.join()) let key = result.strings.join()
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(result.strings.join(), template) TEMPLATE_CACHE.set(key, template)
} }
return template return template
} }
_commitIterable(value) {
if (!isArray(this._$committedValue)) { #commitIterable(value) {
this._$committedValue = [] if (!isArray(this.#value)) {
this.#value = []
this.#clear() this.#clear()
} }
let itemParts = this._$committedValue let itemParts = this.#value
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
)) ))
@ -468,22 +485,40 @@ class ChildPart {
start = node start = node
} }
} }
/**
* 清除已被移除的节点事件
*/
#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
#value = NOTHING
constructor(element, name, strings, decorates, parent, options = {}) { constructor(element, name, strings, decorates, parent, options = {}) {
this.type = ATTRIBUTE_PART
this._$committedValue = NOTHING
this.element = element this.element = element
this.name = name this.name = name
this.decorates = decorates 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._$committedValue = new Array(strings.length - 1).fill(new String()) this.#value = Array(strings.length - 1).fill(null)
this.strings = strings this.strings = strings
} else {
this._$committedValue = NOTHING
} }
} }
get tagName() { get tagName() {
@ -493,10 +528,15 @@ class AttributePart {
$setValue(value, valueIndex) { $setValue(value, valueIndex) {
let strings = this.strings let strings = this.strings
let changed = false let changed = false
if (typeof value === 'function') {
value = value(this.element)
}
if (strings === void 0) { if (strings === void 0) {
changed = !isPrimitive(value) || value !== this._$committedValue changed = !isPrimitive(value) || value !== this.#value
if (changed) { if (changed) {
this._$committedValue = value this.#value = value
} }
} else { } else {
let values = value let values = value
@ -505,41 +545,49 @@ class AttributePart {
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 = values[valueIndex + i]
changed || (changed = !isPrimitive(v) || v !== this._$committedValue[i]) changed || (changed = !isPrimitive(v) || v !== this.#value[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._$committedValue[i] = v this.#value[i] = v
} }
} }
if (changed) { // value属性做特殊处理, 解决输入框无法清空的问题
if (changed || this.name === 'value') {
this.commitValue(value) this.commitValue(value)
} }
} }
commitValue(value) { commitValue(value) {
let isBoolAttr = boolMap[this.name] let elem = this.element
let attr = this.name
let isBoolAttr = boolMap[attr]
// ref属性不渲染到节点上 // ref属性不渲染到节点上
if (this.name === 'ref') { if (attr === 'ref') {
this.options.host.$refs[value] = this.element this.options.host.$refs[value] = elem
return return
} }
if (isBoolAttr) { if (isBoolAttr) {
this.element[isBoolAttr] = !(value === false || value === null) elem[isBoolAttr] = !(value === false || value === null)
if (this.element[isBoolAttr]) { if (elem[isBoolAttr]) {
this.element.setAttribute(this.name, '') elem.setAttribute(attr, '')
} else { } else {
this.element.removeAttribute(this.name) elem.removeAttribute(attr)
} }
} else { } else {
if (value === null || value === void 0) { // value 绑定, 特殊处理
this.element.removeAttribute(this.name) if (attr === 'value') {
elem.value = value
} else { } else {
this.element.setAttribute(this.name, value) if (value === null || value === void 0) {
elem.removeAttribute(attr)
} else {
elem.setAttribute(attr, value)
}
} }
} }
} }
@ -561,20 +609,23 @@ class AnimPart extends AttributePart {
} }
commitValue({ type = 'fade', duration, custom, immediate = false } = {}) { commitValue({ type = 'fade', duration, custom, immediate = false } = {}) {
let elem = this.element
let fromto = MODES[type] || MODES.fade let fromto = MODES[type] || MODES.fade
if (custom) { if (custom) {
fromto = custom fromto = custom
} }
this.element.$animate = function (out = false) { elem.$animate = function (out = false) {
return animate.call(this, duration, fromto, out) return animate.call(this, duration, fromto, out)
} }
this.element.$animate.immediate = immediate elem.$animate.immediate = immediate
} }
} }
// 事件属性 // 事件属性
class EventPart extends AttributePart { class EventPart extends AttributePart {
type = EVENT_PART
#listener = null #listener = null
#prevent = noop #prevent = noop
@ -583,12 +634,14 @@ class EventPart extends AttributePart {
constructor(...args) { constructor(...args) {
super(...args) super(...args)
this.type = EVENT_PART
} }
$setValue(listener) { $setValue(listener) {
let host = this.options.host let host = this.options.host
let elem = this.element
let name = this.name
let options = {} let options = {}
let events = host.$events[name] || []
if (this.decorates.length) { if (this.decorates.length) {
for (let it of this.decorates) { for (let it of this.decorates) {
@ -600,7 +653,7 @@ class EventPart extends AttributePart {
this.#prevent = ev => ev.preventDefault() this.#prevent = ev => ev.preventDefault()
break break
case 'self': case 'self':
this.#checkSelf = ev => ev.target === this.element this.#checkSelf = ev => ev.target === elem
break break
case 'capture': case 'capture':
case 'once': case 'once':
@ -613,42 +666,34 @@ class EventPart extends AttributePart {
let shouldRemove = listener !== this.#listener let shouldRemove = listener !== this.#listener
if (this.#listener && host.$events[this.name]) { if (this.#listener) {
for (let it of host.$events[this.name]) { for (let i = -1, it; (it = events[++i]); ) {
if (it.el === this.element) { if (it.el === elem) {
shouldRemove = shouldRemove =
shouldRemove ||
options.capture !== it.capture || options.capture !== it.capture ||
options.once !== it.once || options.once !== it.once ||
options.passive !== it.passive options.passive !== it.passive
if (shouldRemove) { if (shouldRemove) {
this.element.removeEventListener(this.name, it.listener, it.options) it.el.removeEventListener(name, it.listener, it.options)
events[i] = null
} }
break break
} }
} }
} }
events = events.filter(it => it !== null)
host.$events[name] = events
if (listener && shouldRemove) { if (listener && shouldRemove) {
this.element.addEventListener(this.name, this, options) elem.addEventListener(name, this, options)
this.#listener = listener this.#listener = listener
if (host.$events[this.name]) { events.push({ el: elem, listener: this, options })
host.$events[this.name].push({
el: this.element,
listener: this,
options
})
} else {
host.$events[this.name] = [
{
el: this.element,
listener: this,
options
}
]
}
} }
} }
handleEvent(ev) { handleEvent(ev) {
this.#stop(ev) this.#stop(ev)
this.#prevent(ev) this.#prevent(ev)
@ -660,14 +705,14 @@ class EventPart extends AttributePart {
} }
class ElementPart { class ElementPart {
type = ELEMENT_PART
constructor(element, parent, options) { constructor(element, parent, options) {
this.element = element this.element = element
this.type = ELEMENT_PART
this.$parent = parent this.$parent = parent
this.options = options this.options = options
} }
$setValue(value) {} $setValue() {}
} }
export function render(value, container, options = {}) { export function render(value, container, options = {}) {
@ -682,7 +727,7 @@ export function render(value, container, options = {}) {
) )
container[WC_PART] = part container[WC_PART] = part
} }
part.$setValue(value) part.$setValue(value, true)
return part return part
} }

View File

@ -13,8 +13,7 @@ import {
__props__, __props__,
__changed_props__, __changed_props__,
__mounted__, __mounted__,
__pending__, __pending__
__children__
} 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, raw } from './html.js'
@ -31,6 +30,14 @@ export {
} from './utils.js' } from './utils.js'
export { html, raw, css, svg, bind, unbind, nextTick, fire, hyphen, camelize } 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 = {}) { export function classMap(data = {}) {
let output = '' let output = ''
@ -52,6 +59,42 @@ export function styleMap(data = {}) {
return output 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 {
/** /**
* 声明可监听变化的属性列表 * 声明可监听变化的属性列表
@ -196,9 +239,7 @@ export class Component extends HTMLElement {
} }
this[key] = value this[key] = value
this.$requestUpdate(name) this.$requestUpdate(name)
if (options.observer) { safely.call(this, options.observer, value, oldValue)
options.observer.call(this, value, oldValue)
}
} }
} }
this[key] = options.default this[key] = options.default
@ -218,17 +259,11 @@ export class Component extends HTMLElement {
return value return value
}, },
set: (target, prop, value, receiver) => { set: (target, prop, value, receiver) => {
if (prop === 'length' && options.type === Array) {
return true
}
let oldValue = target[prop] let oldValue = target[prop]
Reflect.set(target, prop, value, receiver) Reflect.set(target, prop, value, receiver)
this.$requestUpdate(key) this.$requestUpdate(key)
safely.call(this, options.observer, value, oldValue)
if (options.observer) {
options.observer.call(this, value, oldValue)
}
return true return true
} }
}) })
@ -296,7 +331,7 @@ export class Component extends HTMLElement {
let options = this.#getPropOptions(name) || {} let options = this.#getPropOptions(name) || {}
let attrName = options.attrName let attrName = options.attrName
if (options.attribute === false) { if (options.attribute === false || options.type === null) {
this.removeAttribute(attrName) this.removeAttribute(attrName)
return return
} }
@ -340,7 +375,7 @@ export class Component extends HTMLElement {
if (options) { if (options) {
value = fixedValue(value, options) value = fixedValue(value, options)
if (options.attribute === false) { if (options.attribute === false || options.type === null) {
if (value === null) { if (value === null) {
return return
} }
@ -388,6 +423,7 @@ export class Component extends HTMLElement {
this.activated() this.activated()
} }
this.mounted() this.mounted()
this.$requestUpdate()
}) })
} else { } else {
nextTick(_ => this.updated(props)) nextTick(_ => this.updated(props))
@ -403,7 +439,7 @@ export class Component extends HTMLElement {
#render() { #render() {
try { try {
let ast = this.render() let ast = this.render()
this[__children__] = render(ast, this.root, { render(ast, this.root, {
host: this host: this
}) })
} catch (err) { } catch (err) {

View File

@ -4,7 +4,7 @@
* @date 2023/03/07 22:11:30 * @date 2023/03/07 22:11:30
*/ */
function noop() {} export function noop() {}
export function $(selector, container, multi) { export function $(selector, container, multi) {
let fn = multi ? 'querySelectorAll' : 'querySelector' let fn = multi ? 'querySelectorAll' : 'querySelector'
@ -108,7 +108,7 @@ 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 isWc = dom && dom.host === dom
@ -121,7 +121,7 @@ export function bind(dom, type = '', selector, fn, phase = true) {
} }
if (typeof selector === 'function') { if (typeof selector === 'function') {
phase = fn phase = fn || false
fn = selector fn = selector
selector = null selector = null
} else { } else {
@ -168,11 +168,21 @@ export function bind(dom, type = '', selector, fn, phase = true) {
events.forEach(function (t) { events.forEach(function (t) {
t = t.trim() t = t.trim()
if (isWc) { if (isWc) {
if (host.$events[t]) { host.$events[t] ??= []
host.$events[t].push({ el: dom, listener: callback, options: phase })
} else { let _list = host.$events[t]
host.$events[t] = [{ el: dom, listener: callback, options: phase }] 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) dom.addEventListener(t, callback, phase)
}) })

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')