精简一波模板解析;优化布尔属性的处理;支持赋值属性的支持

pull/1/head 1.4.0
yutent 2023-03-16 13:40:25 +08:00
parent c632f41e75
commit a57016e14e
5 changed files with 121 additions and 201 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@bd/core", "name": "@bd/core",
"version": "1.3.0", "version": "1.4.0",
"type": "module", "type": "module",
"description": "百搭UI组件库的核心", "description": "百搭UI组件库的核心",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -27,7 +27,8 @@ 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
}) })

View File

@ -1,80 +1,57 @@
import { WC_PART, NO_CHANGE, NOTHING } from './constants.js' import { boolMap, WC_PART, NO_CHANGE, NOTHING } from './constants.js'
var ENABLE_EXTRA_SECURITY_HOOKS = true const boundAttributeSuffix = '$wc$'
const marker = `wc$${String(Math.random()).slice(9)}$`
var identityFunction = value => value const markerMatch = '?' + marker
var noopSanitizer = (_node, _name, _type) => identityFunction const nodeMarker = `<${markerMatch}>`
var setSanitizer = newSanitizer => { const createMarker = (v = '') => document.createComment(v)
if (!ENABLE_EXTRA_SECURITY_HOOKS) { const isPrimitive = value =>
return
}
if (sanitizerFactoryInternal !== noopSanitizer) {
throw new Error(
`Attempted to overwrite existing lit-html security policy. setSanitizeDOMValueFactory should be called at most once.`
)
}
sanitizerFactoryInternal = newSanitizer
}
var _testOnlyClearSanitizerFactoryDoNotCallOrElse = () => {
sanitizerFactoryInternal = noopSanitizer
}
var createSanitizer = (node, name, type) => {
return sanitizerFactoryInternal(node, name, type)
}
var boundAttributeSuffix = '$wc$'
var marker = `wc$${String(Math.random()).slice(9)}$`
var markerMatch = '?' + marker
var nodeMarker = `<${markerMatch}>`
var createMarker = (v = '') => document.createComment(v)
var isPrimitive = value =>
value === null || (typeof value != 'object' && typeof value != 'function') value === null || (typeof value != 'object' && typeof value != 'function')
var isArray = Array.isArray const isArray = Array.isArray
var isIterable = value => const isIterable = value =>
isArray(value) || isArray(value) ||
typeof (value === null || value === void 0 typeof (value === null || value === void 0
? false ? false
: value[Symbol.iterator]) === 'function' : value[Symbol.iterator]) === 'function'
var SPACE_CHAR = `[ \n\f\r]` const SPACE_CHAR = `[ \n\f\r]`
var ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]` const ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]`
var NAME_CHAR = `[^\\s"'>=/]` const NAME_CHAR = `[^\\s"'>=/]`
var textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g const textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g
var COMMENT_START = 1 const COMMENT_START = 1
var TAG_NAME = 2 const TAG_NAME = 2
var DYNAMIC_TAG_NAME = 3 const DYNAMIC_TAG_NAME = 3
var commentEndRegex = /-->/g const commentEndRegex = /-->/g
var comment2EndRegex = />/g const comment2EndRegex = />/g
var tagEndRegex = 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'
) )
var ENTIRE_MATCH = 0 const ENTIRE_MATCH = 0
var ATTRIBUTE_NAME = 1 const ATTRIBUTE_NAME = 1
var SPACES_AND_EQUALS = 2 const SPACES_AND_EQUALS = 2
var QUOTE_CHAR = 3 const QUOTE_CHAR = 3
var singleQuoteAttrEndRegex = /'/g const singleQuoteAttrEndRegex = /'/g
var doubleQuoteAttrEndRegex = /"/g const doubleQuoteAttrEndRegex = /"/g
var rawTextElement = /^(?:script|style|textarea|title)$/i const rawTextElement = /^(?:script|style|textarea|title)$/i
var HTML_RESULT = 1 const HTML_RESULT = 1
var SVG_RESULT = 2 const SVG_RESULT = 2
var ATTRIBUTE_PART = 1 const ATTRIBUTE_PART = 1
var CHILD_PART = 2 const CHILD_PART = 2
var PROPERTY_PART = 3 const EVENT_PART = 5
var BOOLEAN_ATTRIBUTE_PART = 4 const ELEMENT_PART = 6
var EVENT_PART = 5 const COMMENT_PART = 7
var ELEMENT_PART = 6
var COMMENT_PART = 7
var templateCache = new WeakMap() const templateCache = new WeakMap()
var walker = document.createTreeWalker(document, 129, null, false) const walker = document.createTreeWalker(document, 129, null, false)
var sanitizerFactoryInternal = noopSanitizer
var getTemplateHtml = (strings, type) => { function getTemplateHtml(strings, type) {
const len = strings.length - 1 let len = strings.length - 1
const attrNames = [] let attrNames = []
let html2 = type === SVG_RESULT ? '<svg>' : '' let html2 = type === SVG_RESULT ? '<svg>' : ''
let rawTextEndRegex let rawTextEndRegex
let regex = textEndRegex let regex = textEndRegex
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const s = strings[i] let s = strings[i]
let attrNameEndIndex = -1 let attrNameEndIndex = -1
let attrName let attrName
let lastIndex = 0 let lastIndex = 0
@ -131,7 +108,7 @@ var getTemplateHtml = (strings, type) => {
} }
} }
const end = let end =
regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : '' regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : ''
html2 += html2 +=
regex === textEndRegex regex === textEndRegex
@ -147,7 +124,7 @@ var getTemplateHtml = (strings, type) => {
marker + marker +
(attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end) (attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end)
} }
const 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 (!Array.isArray(strings) || !strings.hasOwnProperty('raw')) {
let message = 'invalid template strings array' let message = 'invalid template strings array'
@ -162,44 +139,43 @@ class Template {
let node let node
let nodeIndex = 0 let nodeIndex = 0
let attrNameIndex = 0 let attrNameIndex = 0
const partCount = strings.length - 1 let partCount = strings.length - 1
const parts = this.parts let parts = this.parts
const [html2, attrNames] = getTemplateHtml(strings, type) let [html2, attrNames] = getTemplateHtml(strings, type)
this.el = Template.createElement(html2, options) this.el = Template.createElement(html2, options)
walker.currentNode = this.el.content walker.currentNode = this.el.content
if (type === SVG_RESULT) { if (type === SVG_RESULT) {
const content = this.el.content let content = this.el.content
const svgElement = content.firstChild let svgElement = content.firstChild
svgElement.remove() svgElement.remove()
content.append(...svgElement.childNodes) content.append(...svgElement.childNodes)
} }
while ((node = walker.nextNode()) !== null && parts.length < partCount) { while ((node = walker.nextNode()) !== null && parts.length < partCount) {
if (node.nodeType === 1) { if (node.nodeType === 1) {
if (node.hasAttributes()) { if (node.hasAttributes()) {
const attrsToRemove = [] let attrsToRemove = []
for (const name of node.getAttributeNames()) { for (let name of node.getAttributeNames()) {
if ( if (
name.endsWith(boundAttributeSuffix) || name.endsWith(boundAttributeSuffix) ||
name.startsWith(marker) name.startsWith(marker)
) { ) {
const realName = attrNames[attrNameIndex++] let realName = attrNames[attrNameIndex++]
attrsToRemove.push(name) attrsToRemove.push(name)
if (realName !== void 0) { if (realName !== void 0) {
const value = node.getAttribute( let value = node.getAttribute(
realName.toLowerCase() + boundAttributeSuffix realName.toLowerCase() + boundAttributeSuffix
) )
const statics = value.split(marker) let statics = value.split(marker)
const m = /([.?@])?(.*)/.exec(realName) let m = /([:@])?(.*)/.exec(realName)
parts.push({ parts.push({
type: ATTRIBUTE_PART, type: ATTRIBUTE_PART,
index: nodeIndex, index: nodeIndex,
name: m[2], name: m[2],
strings: statics, strings: statics,
ctor: ctor:
m[1] === '.' m[1] === ':'
? PropertyPart ? PropertyPart
: m[1] === '?'
? BooleanAttributePart
: m[1] === '@' : m[1] === '@'
? EventPart ? EventPart
: AttributePart : AttributePart
@ -212,13 +188,13 @@ class Template {
} }
} }
} }
for (const name of attrsToRemove) { for (let name of attrsToRemove) {
node.removeAttribute(name) node.removeAttribute(name)
} }
} }
if (rawTextElement.test(node.tagName)) { if (rawTextElement.test(node.tagName)) {
const strings2 = node.textContent.split(marker) let strings2 = node.textContent.split(marker)
const lastIndex = strings2.length - 1 let lastIndex = strings2.length - 1
if (lastIndex > 0) { if (lastIndex > 0) {
node.textContent = '' node.textContent = ''
for (let i = 0; i < lastIndex; i++) { for (let i = 0; i < lastIndex; i++) {
@ -230,7 +206,7 @@ class Template {
} }
} }
} else if (node.nodeType === 8) { } else if (node.nodeType === 8) {
const data = node.data let data = node.data
if (data === markerMatch) { if (data === markerMatch) {
parts.push({ type: CHILD_PART, index: nodeIndex }) parts.push({ type: CHILD_PART, index: nodeIndex })
} else { } else {
@ -259,7 +235,7 @@ function resolveDirective(part, value, parent = part, attributeIndex) {
? parent.__directives?.[attributeIndex] ? parent.__directives?.[attributeIndex]
: parent.__directive : parent.__directive
const nextDirectiveConstructor = isPrimitive(value) let nextDirectiveConstructor = isPrimitive(value)
? void 0 ? void 0
: value['_$litDirective$'] : value['_$litDirective$']
@ -294,6 +270,7 @@ function resolveDirective(part, value, parent = part, attributeIndex) {
} }
return value return value
} }
class TemplateInstance { class TemplateInstance {
constructor(template, parent) { constructor(template, parent) {
this._parts = [] this._parts = []
@ -356,7 +333,7 @@ class TemplateInstance {
} }
_update(values) { _update(values) {
let i = 0 let i = 0
for (const 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, part, i) part._$setValue(values, part, i)
@ -369,6 +346,7 @@ class TemplateInstance {
} }
} }
} }
class ChildPart { class ChildPart {
constructor(startNode, endNode, parent, options) { constructor(startNode, endNode, parent, options) {
this.type = CHILD_PART this.type = CHILD_PART
@ -379,16 +357,13 @@ class ChildPart {
this._$parent = parent this._$parent = parent
this.options = options this.options = options
this.__isConnected = options?.isConnected || true this.__isConnected = options?.isConnected || true
if (ENABLE_EXTRA_SECURITY_HOOKS) {
this._textSanitizer = void 0
}
} }
get _$isConnected() { get _$isConnected() {
return this._$parent?._$isConnected || this.__isConnected return this._$parent?._$isConnected || this.__isConnected
} }
get parentNode() { get parentNode() {
let parentNode = this._$startNode.parentNode let parentNode = this._$startNode.parentNode
const 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
} }
@ -405,7 +380,7 @@ class ChildPart {
if (isPrimitive(value)) { if (isPrimitive(value)) {
if (value === NOTHING || value == null || value === '') { if (value === NOTHING || value == null || value === '') {
if (this._$committedValue !== NOTHING) { if (this._$committedValue !== NOTHING) {
this._$clear() this.#clear()
} }
this._$committedValue = NOTHING this._$committedValue = NOTHING
} else if (value !== this._$committedValue && value !== NO_CHANGE) { } else if (value !== this._$committedValue && value !== NO_CHANGE) {
@ -426,17 +401,7 @@ class ChildPart {
} }
_commitNode(value) { _commitNode(value) {
if (this._$committedValue !== value) { if (this._$committedValue !== value) {
this._$clear() this.#clear()
if (
ENABLE_EXTRA_SECURITY_HOOKS &&
sanitizerFactoryInternal !== noopSanitizer
) {
const parentNodeName = this._$startNode.parentNode?.nodeName
if (parentNodeName === 'STYLE' || parentNodeName === 'SCRIPT') {
throw new Error('Forbidden')
}
}
this._$committedValue = this._insert(value) this._$committedValue = this._insert(value)
} }
@ -446,34 +411,17 @@ class ChildPart {
this._$committedValue !== NOTHING && this._$committedValue !== NOTHING &&
isPrimitive(this._$committedValue) isPrimitive(this._$committedValue)
) { ) {
const node = this._$startNode.nextSibling let node = this._$startNode.nextSibling
if (ENABLE_EXTRA_SECURITY_HOOKS) {
if (this._textSanitizer === void 0) {
this._textSanitizer = createSanitizer(node, 'data', 'property')
}
value = this._textSanitizer(value)
}
node.data = value node.data = value
} else {
if (ENABLE_EXTRA_SECURITY_HOOKS) {
const textNode = document.createTextNode('')
this._commitNode(textNode)
if (this._textSanitizer === void 0) {
this._textSanitizer = createSanitizer(textNode, 'data', 'property')
}
value = this._textSanitizer(value)
textNode.data = value
} else { } else {
this._commitNode(document.createTextNode(value)) this._commitNode(document.createTextNode(value))
} }
}
this._$committedValue = value this._$committedValue = value
} }
_commitTemplateResult(result) { _commitTemplateResult(result) {
const { values, ['__dom_type__']: type } = result let { values, ['__dom_type__']: type } = result
const template = let template =
typeof type === 'number' typeof type === 'number'
? this._$getTemplate(result) ? this._$getTemplate(result)
: (type.el === void 0 && : (type.el === void 0 &&
@ -483,8 +431,8 @@ class ChildPart {
if (this._$committedValue?._$template === template) { if (this._$committedValue?._$template === template) {
this._$committedValue._update(values) this._$committedValue._update(values)
} else { } else {
const instance = new TemplateInstance(template, this) let instance = new TemplateInstance(template, this)
const fragment = instance._clone(this.options) let fragment = instance._clone(this.options)
instance._update(values) instance._update(values)
@ -502,12 +450,12 @@ class ChildPart {
_commitIterable(value) { _commitIterable(value) {
if (!isArray(this._$committedValue)) { if (!isArray(this._$committedValue)) {
this._$committedValue = [] this._$committedValue = []
this._$clear() this.#clear()
} }
const itemParts = this._$committedValue let itemParts = this._$committedValue
let partIndex = 0 let partIndex = 0
let itemPart let itemPart
for (const 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(
@ -524,11 +472,12 @@ class ChildPart {
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) {
this._$notifyConnectionChanged?.call(this, false, true, from) this._$notifyConnectionChanged?.call(this, false, true, from)
while (start && start !== this._$endNode) { while (start && start !== this._$endNode) {
@ -544,6 +493,7 @@ class ChildPart {
} }
} }
} }
// 常规属性
class AttributePart { class AttributePart {
constructor(element, name, strings, parent, options) { constructor(element, name, strings, parent, options) {
this.type = ATTRIBUTE_PART this.type = ATTRIBUTE_PART
@ -559,9 +509,6 @@ class AttributePart {
} else { } else {
this._$committedValue = NOTHING this._$committedValue = NOTHING
} }
if (ENABLE_EXTRA_SECURITY_HOOKS) {
this._sanitizer = void 0
}
} }
get tagName() { get tagName() {
return this.element.tagName return this.element.tagName
@ -569,8 +516,9 @@ class AttributePart {
get _$isConnected() { get _$isConnected() {
return this._$parent._$isConnected return this._$parent._$isConnected
} }
_$setValue(value, directiveParent = this, valueIndex, noCommit) { _$setValue(value, directiveParent = this, valueIndex, noCommit) {
const strings = this.strings let strings = this.strings
let change = false let change = false
if (strings === void 0) { if (strings === void 0) {
value = resolveDirective(this, value, directiveParent, 0) value = resolveDirective(this, value, directiveParent, 0)
@ -581,7 +529,7 @@ class AttributePart {
this._$committedValue = value this._$committedValue = value
} }
} else { } else {
const 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++) {
@ -604,70 +552,45 @@ class AttributePart {
} }
} }
if (change && !noCommit) { if (change && !noCommit) {
this._commitValue(value) this.#commitValue(value)
} }
} }
_commitValue(value) {
if (value === NOTHING) {
this.element.removeAttribute(this.name)
} else {
if (ENABLE_EXTRA_SECURITY_HOOKS) {
if (this._sanitizer === void 0) {
this._sanitizer = sanitizerFactoryInternal(
this.element,
this.name,
'attribute'
)
}
value = this._sanitizer(value !== null && value !== void 0 ? value : '')
}
this.element.setAttribute( #commitValue(value) {
this.name, let isBoolAttr = boolMap[this.name]
value !== null && value !== void 0 ? value : ''
)
}
}
}
class PropertyPart extends AttributePart {
constructor() {
super(...arguments)
this.type = PROPERTY_PART
}
_commitValue(value) {
if (ENABLE_EXTRA_SECURITY_HOOKS) {
if (this._sanitizer === void 0) {
this._sanitizer = sanitizerFactoryInternal(
this.element,
this.name,
'property'
)
}
value = this._sanitizer(value)
}
this.element[this.name] = value === NOTHING ? void 0 : value if (isBoolAttr) {
} this.element[isBoolAttr] = !(value === false || value === null)
}
class BooleanAttributePart extends AttributePart { if (this.element[isBoolAttr]) {
constructor() {
super(...arguments)
this.type = BOOLEAN_ATTRIBUTE_PART
}
_commitValue(value) {
if (value && value !== NOTHING) {
// 布尔属性,值固定为空
this.element.setAttribute(this.name, '') this.element.setAttribute(this.name, '')
} else { } else {
this.element.removeAttribute(this.name) this.element.removeAttribute(this.name)
} }
} else {
if (value === null || value === void 0) {
this.element.removeAttribute(this.name)
} else {
this.element.setAttribute(this.name, value)
}
}
}
}
// 赋值属性
class PropertyPart extends AttributePart {
constructor(...args) {
super(...args)
}
#commitValue(value) {
this.element[this.name] = value
} }
} }
// 事件属性
class EventPart extends AttributePart { class EventPart extends AttributePart {
constructor(element, name, strings, parent, options) { constructor(...args) {
super(element, name, strings, parent, options) super(...args)
this.type = EVENT_PART this.type = EVENT_PART
} }
_$setValue(newListener, directiveParent = this) { _$setValue(newListener, directiveParent = this) {
@ -677,13 +600,13 @@ class EventPart extends AttributePart {
if (newListener === NO_CHANGE) { if (newListener === NO_CHANGE) {
return return
} }
const oldListener = this._$committedValue let oldListener = this._$committedValue
const shouldRemoveListener = let shouldRemoveListener =
(newListener === NOTHING && oldListener !== NOTHING) || (newListener === NOTHING && oldListener !== NOTHING) ||
newListener.capture !== oldListener.capture || newListener.capture !== oldListener.capture ||
newListener.once !== oldListener.once || newListener.once !== oldListener.once ||
newListener.passive !== oldListener.passive newListener.passive !== oldListener.passive
const shouldAddListener = let shouldAddListener =
newListener !== NOTHING && newListener !== NOTHING &&
(oldListener === NOTHING || shouldRemoveListener) (oldListener === NOTHING || shouldRemoveListener)
@ -703,6 +626,7 @@ class EventPart extends AttributePart {
} }
} }
} }
class ElementPart { class ElementPart {
constructor(element, parent, options) { constructor(element, parent, options) {
this.element = element this.element = element
@ -749,8 +673,3 @@ export const svg = (strings, ...values) => {
values values
} }
} }
if (ENABLE_EXTRA_SECURITY_HOOKS) {
render.setSanitizer = setSanitizer
render.createSanitizer = createSanitizer
}

View File

@ -235,12 +235,12 @@ export class Component extends HTMLElement {
unmounted() {} unmounted() {}
updated() {} updated() {}
$on(type, callback) { $on(type, callback, options) {
return bind(this, type, callback) return bind(this, type, callback, options)
} }
$off(type, callback) { $off(type, callback, options) {
unbind(this, type, callback) unbind(this, type, callback, options)
} }
$emit(type, data = {}) { $emit(type, data = {}) {

View File

@ -13,9 +13,9 @@ export function $(selector, container) {
export function $$(selector, container) { export function $$(selector, container) {
if (container) { if (container) {
return container.querySelectorsAll(selector) return container.querySelectorAll(selector)
} }
return document.body.querySelectorsAll(selector) return document.body.querySelectorAll(selector)
} }
export const nextTick = (function () { export const nextTick = (function () {