Created
August 4, 2022 12:30
-
-
Save Mapaler/07b643000e5ef24535a1a860d957ed4d to your computer and use it in GitHub Desktop.
Simplified custom DOMTokenList implementation. 自己实现的具有 DOMTokenList 功能的类。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Detail: 仿照 DOMTokenList 功能实现的自定义 CustomTokenList Version2 (extends Array) | |
* Reference: https://gist.github.com/dimitarnestorov/48b69a918288e9098db1aab904a2722a | |
* Use: | |
let span = document.querySelector("span"); | |
let _ctl = new CustomTokenList(span, "custom-token"); | |
span.customTokenList = _ctl; | |
span.customTokenList.add("token1","token2"); | |
console.log(span.getAttribute("custom-token")); | |
// token1 token2 | |
await span.setAttribute("custom-token", "token-a token-b token-c"); | |
console.log(span.customTokenList.value); | |
// token-a token-b token-c | |
*/ | |
class CustomTokenList extends Array { | |
#node = null; | |
#attributeName = null; | |
#refreshAttribute = true; | |
#observer = null; | |
#observerOptions = { | |
attributeFilter: [], | |
attributeOldValue: true, | |
subtree: true | |
}; | |
get #attribute() { //获取绑定的参数 | |
return this.#node.getAttributeNode(this.#attributeName); | |
} | |
set #attribute(node) { //直接设定绑定的参数AttrNode | |
if (Object.getPrototypeOf(node).constructor == Attr) { | |
this.#node = node.ownerElement; | |
this.#attributeName = node.nodeName; | |
this.#observerOptions.attributeFilter = [this.#attributeName]; | |
} else { | |
throw new TypeError(`${CustomTokenList.name}.#attribute: Argument 1 is not an Attr.\n参数 1 不是 Attr。`); | |
} | |
} | |
constructor(node, attributeName){ //传入 HTMLElement 和需要绑定的 参数名称 | |
super(); | |
if (Object.getPrototypeOf(node).constructor == Attr) { | |
this.#attribute = node; | |
} else if (node instanceof HTMLElement) { | |
this.#node = node; | |
this.#attributeName = attributeName.toString().toLowerCase(); //attributeName 只支持小写 | |
this.#observerOptions.attributeFilter = [this.#attributeName]; | |
} else { | |
throw new TypeError(`${CustomTokenList.name}.constructor: Argument 1 is not an Attr or HTMLElement.\n参数 1 不是 Attr 或 HTMLElement。`); | |
} | |
const _this = this; | |
this.#observer = new MutationObserver(function(mutationList) { | |
for (const mutation of mutationList) { | |
if (mutation.type == 'attributes' && mutation.attributeName == _this.#attributeName) { | |
_this.#attribute = _this.#node.getAttributeNode(_this.#attributeName); | |
_this.length = 0; | |
if (_this.#attribute) { | |
_this.#refreshAttribute = false; //外部属性变化时,添加内容不再循环进行属性的更新 | |
_this.add(..._this.#attribute.nodeValue.split(/\s+/g)); | |
_this.#refreshAttribute = true; | |
} | |
} | |
} | |
}); | |
this.#observer.observe(this.#node, this.#observerOptions); | |
} | |
static #InvalidCharacterError(functionName) { | |
return new DOMException(`${CustomTokenList.name}.${functionName}:The token can not contain whitespace.\bToken 不允许包含空格。`, "InvalidCharacterError"); | |
} | |
add(...tokens){ | |
//全部强制转换为字符串 | |
tokens = tokens.map(token=>token.toString()); | |
//如果任何 token 里存在空格,就直接抛出错误 | |
if (tokens.some(token=>/\s/.test(token))) | |
throw CustomTokenList.#InvalidCharacterError('add'); | |
tokens.forEach(token => { | |
if (!this.includes(token)) this.push(token); | |
}); | |
if (this.#refreshAttribute) { | |
this.#observer.disconnect(); //解除绑定 | |
if (!this.#attribute) { | |
this.#node.setAttributeNode(document.createAttribute(this.#attributeName)); | |
} | |
this.#attribute.value = this.join(' '); | |
this.#observer.observe(this.#node, this.#observerOptions); //恢复绑定 | |
} | |
return; | |
} | |
remove(...tokens){ | |
//全部强制转换为字符串 | |
tokens = tokens.map(token=>token.toString()); | |
//如果任何 token 里存在空格,就直接抛出错误 | |
if (tokens.some(token=>/\s/.test(token))) | |
throw CustomTokenList.#InvalidCharacterError('remove'); | |
tokens.forEach(token => { | |
const index = this.indexOf(token); | |
if (index>=0) this.splice(index,1); | |
}); | |
if (this.#refreshAttribute) { | |
this.#observer.disconnect(); //解除绑定 | |
this.#attribute.value = this.join(' '); | |
this.#observer.observe(this.#node, this.#observerOptions); //恢复绑定 | |
} | |
return; | |
} | |
contains(token){ | |
return this.includes(token.toString()); | |
} | |
toggle(token, force){ | |
if (/\s/.test(token)) | |
throw CustomTokenList.#InvalidCharacterError('toggle'); | |
if (force !== undefined) { | |
if(Boolean(force)) { | |
this.add(token); | |
return true; | |
}else{ | |
this.remove(token); | |
return false; | |
} | |
} else { | |
if (this.contains(token)) { | |
this.remove(token); | |
return false; | |
} else { | |
this.add(token); | |
return true; | |
} | |
} | |
} | |
get value() { | |
return this.join(' '); | |
} | |
replace(oldToken, newToken){ | |
oldToken = oldToken.toString(); | |
newToken = newToken.toString(); | |
if (/\s/.test(oldToken) || /\s/.test(newToken)) | |
throw CustomTokenList.#InvalidCharacterError('replace'); | |
const index = this.indexOf(oldToken); | |
if (index>=0) { | |
this.splice(index,1, newToken); | |
if (this.#refreshAttribute) { | |
this.#observer.disconnect(); //解除绑定 | |
this.#attribute.value = this.join(' '); | |
this.#observer.observe(this.#node, this.#observerOptions); //恢复绑定 | |
} | |
return true; | |
} else { | |
return false; | |
} | |
} | |
item(index) { | |
return this[index]; | |
} | |
}; | |
/* | |
* Detail: 仿照 DOMTokenList 功能实现的自定义 CustomTokenList Version1 (Object) | |
* Reference: https://gist.github.com/dimitarnestorov/48b69a918288e9098db1aab904a2722a | |
* Use: | |
let span = document.querySelector("span"); | |
let _ctl = new CustomTokenList(span, "custom-token"); | |
span.customTokenList = _ctl; | |
span.customTokenList.add("token1","token2"); | |
console.log(span.getAttribute("custom-token")); | |
// token1 token2 | |
await span.setAttribute("custom-token", "token-a token-b token-c"); | |
console.log(span.customTokenList.value); | |
// token-a token-b token-c | |
*/ | |
class CustomTokenList { | |
tokens = new Set(); | |
#node = null; | |
#attributeName = null; | |
#refreshAttribute = true; | |
#observer = null; | |
#observerOptions = { | |
attributeFilter: [], | |
attributeOldValue: true, | |
subtree: true | |
}; | |
get #attribute() { //获取绑定的参数 | |
return this.#node.getAttributeNode(this.#attributeName); | |
} | |
set #attribute(node) { //直接设定绑定的参数AttrNode | |
if (Object.getPrototypeOf(node).constructor == Attr) { | |
this.#node = node.ownerElement; | |
this.#attributeName = node.nodeName; | |
this.#observerOptions.attributeFilter = [this.#attributeName]; | |
} else { | |
throw new TypeError(`${CustomTokenList.name}.#attribute: Argument 1 is not an Attr.\n参数 1 不是 Attr。`); | |
} | |
} | |
constructor(node, attributeName){ //传入 HTMLElement 和需要绑定的 参数名称 | |
if (Object.getPrototypeOf(node).constructor == Attr) { | |
this.#attribute = node; | |
} else if (node instanceof HTMLElement) { | |
this.#node = node; | |
this.#attributeName = attributeName.toString().toLowerCase(); | |
this.#observerOptions.attributeFilter = [this.#attributeName]; | |
} else { | |
throw new TypeError(`${CustomTokenList.name}.constructor: Argument 1 is not an Attr or HTMLElement.\n参数 1 不是 Attr 或 HTMLElement。`); | |
} | |
const _this = this; | |
this.#observer = new MutationObserver(function(mutationList) { | |
for (const mutation of mutationList) { | |
if (mutation.type == 'attributes' && mutation.attributeName == _this.#attributeName) { | |
_this.#attribute = _this.#node.getAttributeNode(_this.#attributeName); | |
_this.tokens.clear(); | |
if (_this.#attribute) { | |
_this.#refreshAttribute = false; //外部属性变化时,添加内容不再循环进行属性的更新 | |
_this.add(..._this.#attribute.nodeValue.split(/\s+/g)); | |
_this.#refreshAttribute = true; | |
} | |
} | |
} | |
}); | |
this.#observer.observe(this.#node, this.#observerOptions); | |
} | |
static #InvalidCharacterError(functionName) { | |
return new DOMException(`${CustomTokenList.name}.${functionName}:The token can not contain whitespace.\bToken 不允许包含空格。`, "InvalidCharacterError"); | |
} | |
add(...tokens){ | |
//全部强制转换为字符串 | |
tokens = tokens.map(token=>token.toString()); | |
//如果任何 token 里存在空格,就直接抛出错误 | |
if (tokens.some(token=>/\s/.test(token))) | |
throw CustomTokenList.#InvalidCharacterError('add'); | |
tokens.forEach(token=>this.tokens.add(token)); | |
if (this.#refreshAttribute) { | |
this.#observer.disconnect(); //解除绑定 | |
if (!this.#attribute) { | |
this.#node.setAttributeNode(document.createAttribute(this.#attributeName)); | |
} | |
this.#attribute.value = [...this.tokens].join(' '); | |
this.#observer.observe(this.#node, this.#observerOptions); //恢复绑定 | |
} | |
return; | |
} | |
remove(...tokens){ | |
//全部强制转换为字符串 | |
tokens = tokens.map(token=>token.toString()); | |
//如果任何 token 里存在空格,就直接抛出错误 | |
if (tokens.some(token=>/\s/.test(token))) | |
throw CustomTokenList.#InvalidCharacterError('remove'); | |
tokens.forEach(token=>this.tokens.delete(token)); | |
if (this.#refreshAttribute) { | |
this.#observer.disconnect(); //解除绑定 | |
this.#attribute.value = [...this.tokens].join(' '); | |
this.#observer.observe(this.#node, this.#observerOptions); //恢复绑定 | |
} | |
return; | |
} | |
contains(token){ | |
return this.tokens.has(token.toString()); | |
} | |
toggle(token, force){ | |
if (/\s/.test(token)) | |
throw CustomTokenList.#InvalidCharacterError('toggle'); | |
if (force !== undefined) { | |
if(Boolean(force)) { | |
this.add(token); | |
return true; | |
}else{ | |
this.remove(token); | |
return false; | |
} | |
} else { | |
if (this.contains(token)) { | |
this.remove(token); | |
return false; | |
} else { | |
this.add(token); | |
return true; | |
} | |
} | |
} | |
forEach(callback, thisArg) { | |
[...this.tokens].forEach( | |
(value, index, array)=> | |
callback.call(thisArg ?? this, value, index, array) | |
); | |
} | |
entries() { | |
return [...this.tokens].entries(); | |
} | |
get value() { | |
return [...this.tokens].join(' '); | |
} | |
values() { | |
return this.tokens.values(); | |
} | |
keys() { | |
return new Array(this.tokens).keys(); | |
} | |
replace(oldToken, newToken){ | |
if (/\s/.test(oldToken) || /\s/.test(newToken)) | |
throw CustomTokenList.#InvalidCharacterError('replace'); | |
if (this.contains(oldToken)) { | |
this.#refreshAttribute = false; //减少一次属性的更新 | |
this.remove(oldToken); | |
this.#refreshAttribute = true; | |
this.add(newToken); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
item(index) { | |
return [...this.tokens][index]; | |
} | |
valueOf() { | |
return this.tokens; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment