DOM 原本的 API 在撰寫上冗長且操作便,在這邊將 DOM API 做簡單封裝,並建立一個 XD
模組,首先,在 XD-1.0.0.js 中建立一些常數與函式:
// 標準化屬性名稱
const PROPS = new Map([
['for', 'htmlFor'],
['class', 'className'],
['readonly', 'readOnly'],
['maxlength', 'maxLength'],
['cellspacing', 'cellSpacing'],
['rowspan', 'rowSpan'],
['colspan', 'colSpan'],
['tabindex', 'tabIndex'],
['usemap', 'useMap'],
['frameborder', 'frameBorder']
]);
// 判斷元素的類型
function isElementNode(elem) {
return elem.nodeType === Node.ELEMENT_NODE;
}
function isTextNode(elem) {
return elem.nodeType === Node.TEXT_NODE;
}
function isCommentNode(elem) {
return elem.nodeType === Node.COMMENT_NODE;
}
function isInputNode(elem) {
return elem.nodeName === 'INPUT';
}
雖然要修補 JavaScript 的物件非常容易,除非是為了相容於新的標準,否則不建議在任何原生物件或 DOM 物件上添加特性,以免開發者無法辨別,這些特性是原生的或者來自於程式庫,因此,通常會採取包裹器的形式,將原生物件或 DOM 物件等包裹,開發者建立並操作包裹器,由包裹器來操作原生 API。
因此,接下來在 XD-1.0.0.js 中定義 ElemCollection
類別:
class ElemCollection {
// 建構時傳入原生 DOM 物件的 Array 清單
constructor(elems) {
this.elems = elems;
}
// 指定索引取得元素
get(index = 0) {
return this.elems[index];
}
// 包裹器管理的 DOM 物件個數
size() {
return this.elems.length;
}
// 包裹器中的 DOM 元素清單是否為空
isEmpty() {
return this.elems.length === 0;
}
// 逐一操作管理的 DOM 元素
each(consume) {
this.elems.forEach(consume);
return this;
}
// 如果 value 為 undefined,傳回第一個 DOM 元素的 innerHTML
// 否則用 value 設定全部 DOM 元素之 innerHTML
html(value) {
let elems = this.elems;
if(value === undefined) {
return elems[0] && isElementNode(elems[0]) ? elems[0].innerHTML : null;
}
else {
elems.filter(isElementNode)
.forEach(elem => elem.innerHTML = value);
return this;
}
}
// 如果 value 為 undefined,傳回第一個 DOM 元素的屬性對應之特性
// 否則用 value 設定全部 DOM 元素之指定特性
attr(name, value) {
let elems = this.elems;
let propName = PROPS.has(name) ? PROPS.get(name) : name;
if(value === undefined) {
return elems[0] && !isTextNode(elems[0]) && !isCommentNode(elems[0]) ?
elems[0][propName] : undefined;
}
else {
elems.filter(elem => !isTextNode(elem) && !isCommentNode(elem))
.forEach(elem => elem[propName] = value);
return this;
}
}
// 如果 value 為 undefined,傳回第一個 input 元素的 value
// 否則用 value 設定全部 input 元素的 value
val(value) {
let elems = this.elems;
// 先只處理 <input> 元素
if(value === undefined) {
return elems[0] && isInputNode(elems[0]) ? elems[0].value : null;
}
else {
elems.filter(isInputNode)
.forEach(elem => elem.value = value);
return this;
}
}
// 如果只有一個父節點,將指定的 elemsCollection 管理之元素附加至該節點
// 否則用複製 elemsCollection 管理之元素,再附加至各個父節點
append(elemsCollection) {
let parents = this.elems;
if(parents.length === 1) { // 只有一個父節點
let parent = parents[0];
elemsCollection.each(elem => parent.appendChild(elem));
}
else if(parents.length > 1){ // 有多個父節點
parents.forEach(parent => {
elemsCollection.each(elem => {
// 複製子節點
var container = document.createElement('div');
container.appendChild(elem);
container.innerHTML = container.innerHTML;
parent.appendChild(container.firstChild);
});
});
}
return this;
}
// 將管理之元素從 DOM 樹上移除
remove() {
this.elems.forEach(elem => {
elem.parentNode.removeChild(elem);
});
return this;
}
}
接著在選取元素上,基於原生的 getElementById
、getElementsByTagName
、getElementsByName
、querySelectorAll
等 API 來建立包裹器:
function elemsById(...ids) {
let container = this || document;
let elems = ids.map(id => container.getElementById(id));
return new ElemCollection(elems);
}
function elemsByTag(...tags) {
let container = this || document;
let elems = tags.map(tag => Array.from(container.getElementsByTagName(tag)))
.reduce((acc, arr) => acc.concat(arr), []);
return new ElemCollection(elems);
}
function elemsByName(...names) {
let container = this || document;
let elems = names.map(name => Array.from(container.getElementsByName(name)))
.reduce((acc, arr) => acc.concat(arr), []);
return new ElemCollection(elems);
}
function elemsBySelector(...selectors) {
let container = this || document;
let elems = selectors.map(selector => Array.from(container.querySelectorAll(selector)))
.reduce((acc, arr) => acc.concat(arr), []);
return new ElemCollection(elems);
}
// 指定一或多個標籤名稱,建立 DOM 元素
function create(...tags) {
return new ElemCollection(tags.map(tag => document.createElement(tag)));
}
這幾個函式的名稱會是模組匯出的名稱:
export {elemsById, elemsByTag, elemsByName, elemsBySelector, create};
除了建立包裹器來管理一組 DOM 元素外,也可以有個包裹器來包裹單一 DOM 元素:
// 包裹單一 DOM 元素
class XD {
constructor(elem) {
this.elem = elem;
}
elemsById(...ids) {
return elemsById.apply(this.elem, ids);
}
elemsByTag(...tags) {
return elemsByTag.apply(this.elem, tags);
}
elemsByName(...names) {
return elemsByName.apply(this.elem, names);
}
elemsBySelector(...selectors) {
return elemsBySelector.apply(this.elem, selectors);
}
toElemCollection() {
return new ElemCollection([this.elem]);
}
}
// 預設匯出的工廠函式,用來建立 X 實例
// 如果傳入字串,會建立新元素
// 否則直接包裹 DOM 元素
export default function(elem) {
if(typeof elem === 'string') {
return new XD(document.createElement(elem));
}
return new XD(elem);
}
現在,若 XD-1.0.0.js 放在 js 資料夾中,若想使用這個 XD
模組,可以在 HTML 頁面中如下撰寫程式(你的瀏覽器必須支援 ES6 模組):
<script type="module">
import {elemsById, elemsByTag, elemsByName, elemsBySelector} from './js/XD-1.0.0.js';
elemsById('console', 'cmd').html('<b>Hello, World</b>');
console.log(elemsBySelector('#console').html());
elemsByTag('span', 'div').attr('class', 'red')
.html('<i>Red Color</i>');
elemsByName('name').val('100')
.each(elem => console.log(elem.value));
</script>
或者可以使用預設匯入:
<script type="module">
import x from './js/XD-1.0.0.js';
let doc = x(document);
doc.elemsById('console', 'cmd').html('<b>Hello, World</b>');
console.log(doc.elemsBySelector('#console').html());
doc.elemsByTag('span', 'div').attr('class', 'red')
.html('<i>Red Color</i>');
doc.elemsByName('name').val('100')
.each(elem => console.log(elem.value));
</script>
來用在先前的範例上,看看改寫之後會長什麼樣,首先改寫一下〈修改文件〉中的第一個範例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<input id="src" type="text"><button id="add">新增圖片</button>
<div id="images"></div>
<script type="module">
import {elemsById, create} from './js/XD-1.0.0.js';
elemsById('add').get().onclick = function() {
let img = create('img');
img.attr('src', elemsById('src').val())
.get()
.onclick = function() {
img.remove();
};
elemsById('images').append(img);
};
</script>
</body>
</html>
來稍微簡化一下〈修改文件〉中第二個範例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
容器一:
<div id="container1">
<img id="image" src="https://openhome.cc/Gossip/images/caterpillar_small.jpg"/>
</div><br>
容器二:
<div id="container2"></div>
<script type="module">
import {elemsById} from './js/XD-1.0.0.js';
let image = elemsById('image');
image.get().onclick = function() {
let c1 = elemsById('container1');
let c2 = elemsById('container2');
if(this.parentNode === c1.get()) {
c2.append(image);
} else {
c1.append(image);
}
};
</script>
</body>
</html>
這兩個範例的事件處理,還沒有進一步做適當地封裝,因而看來風格不一致,這會是後續討論事件處理時的主題。
完整的 XD-1.0.0.js 可以按此下載。