封裝 DOM 操作



DOM原本的API在撰寫上不僅冗長,而且不方便,在這邊將為DOM API作為簡單的封裝,這邊以 建立核心公用函式 為基礎,繼續API的封裝。首先,調整一下原有的程式為如下的架構,相關說明直接撰寫於註解中:
(function(global) {
    // 現在讓 XD 參考至函式,並作為名稱空間
    var XD = function(selector, container) {
        return new XD.mth.init(selector, container);
    };

    // 公用函式先放在一個暫時物件集中
    var utils = {
        trim: function(text) {
            return (text || '').replace( /^(\s|\u00A0)+|(\s|\u00A0)+\$/g, '');
        },
        ...
    };
   
    // 這個函式可以將source上的特性合併至target上成為特性
    function extend(target, source) {
        utils.each(source, function(value, key) {
            target[key] = value;
        });
    }
  
    // 將utils上的特性合併至XD
    extend(XD, utils);
    //extend函式亦可為公用函式,故以XD作為名稱空間
    XD.extend = extend;
    // 標準化屬性名稱
    XD.props = {
        'for': 'htmlFor',
        'class': 'className',
        readonly: 'readOnly',
        maxlength: 'maxLength',
        cellspacing: 'cellSpacing',
        rowspan: 'rowSpan',
        colspan: 'colSpan',
        tabindex: 'tabIndex',
        usemap: 'useMap',
        frameborder: 'frameBorder'
    };

    // 這邊以下再解釋
    XD.mth = XD.prototype = {
       init: function(selector, container) {
            ...
       }
    };
  
    XD.mth.init.prototype = XD.mth;

    global.XD = XD;
})(this);

調整為以上架構的目的在於,希望XD()函式可以封裝getElementById()與getElementsByTagName()的操作。例如,你可以傳入id
XD('#xyz');  // 傳回一個物件,物件索引0是id為xyz的元素。
XD('#xyz, #abc');  // 傳回一個物件,物件索引0是id為xyz的元素、索引1是id為abc的元素
XD('#xyz, #abc, #123'); // 如上類推,索引2是id為123的元素

也可以傳入標籤名:
XD('img');  // 傳回一個物件,收集了所有img元素,可使用索引存取
XD('img, div');  // 傳回一個物件,收集了所有img與div元素,可使用索引存取
XD('img, div, a'); // 傳回一個物件,收集了所有img、div、a元素,可使用索引存取
XD('img', XD('#container')); // 以id為container的元素開始,取得img元素

或者是傳入標籤來建立元素。例如:
XD('<img>');       // 建立img元素,收集在傳回的物件
XD('<img>,<div>'); // 建立img、div元素,收集在傳回的物件

從上面的架構知道,呼叫函式XD(),內部會建立XD.mth.init()的實例,而XD.mth.init()的實作如下:
    XD.mth = XD.prototype = {
       init: function(selector, container) {
           // 這是元素
           if(selector.nodeType) {
               this[0] = selector;
               this.length = 1;
               return this;
           }
           // 如果有指定容器(XD()傳回的物件)
           // 使用容器上第一個元素來呼叫getElementById()或getElementsByTagName()

           if(container && container[0]) {
               container = container[0];
           }
           else { // 否則,使用document
               container = document;
           }
           // 先用Array收集元素
           var elements = [];
           // 可以指定多個,用,區隔
           XD.each(selector.split(','), function(text) {
               text = XD.trim(text);
               if(text.charAt(0) === '#') { // 這是指定id
                   elements.push(container.getElementById(text.substring(1)));
               }
               else if(text.charAt(0) === '<') { // 這是指定<tag>
                   elements.push(document.createElement(
                                     text.substring(1, text.length - 1)));
               }
               else { // 否則就是指定標籤
                   XD.each(container.getElementsByTagName(text), function(element) {
                       elements.push(element);
                   });
                  
               }
           });
           // 將Array上的元素複製至this
           XD.extend(this, elements);
           // 順便指定一下收集了幾個元素
           this.length = elements.length;
       }
   };

要記得,new 之後接的函式若無傳回值,預設是傳回this。在上頭的程式中,XD.mth與XD.prototype都是參考至同一物件,之後 XD.mth.init.prototype再參考至XD.mth,這表示,透過XD.mth.init建立的實例,將可以取得XD.mth上的方法。例 如,如果在物件上新增一些簡單的方法:
   XD.mth = XD.prototype = {
       init: function(selector, container) {
           ...
       },
       size: function() { // 收集了幾個物件
           return this.length;
       },
       isEmpty: function() {  // 是否為空(沒有收集半個物件)
           return this.length === 0;
       }
   };

假設文件中有兩個<img>,則你可以如下收集並得到size結果為2,isEmpty為false:
var size = XD('img').size();        // 2
var isEmpty = XD('img').isEmpty();  // false

這邊再示範幾個方法的建立:
   XD.mth = XD.prototype = {
       ...

       each: function(callback) {
           return XD.each(this, callback);
       },
       html: function(value) {
           if(value === undefined) {
               return this[0] && this[0].nodeType === 1 ?
                        this[0].innerHTML : null;
           }
           else {
               return XD.each(this, function(element) {
                   if(element.nodeType === 1) {
                       element.innerHTML = value;
                   }
               });
           }
       },
       attr: function(name, value) {
           name = XD.props[name] || name;
           if(value === undefined) {
               return this[0] && this[0].nodeType !== 3 && this[0].nodeType !== 8 ?
                        this[0][name] : undefined;
           }
           else {
               return XD.each(this, function(element) {
                   if(element.nodeType !== 3 && element.nodeType !== 8) {
                       element[name] = value;
                   }
               });
           }      
       },
       val: function(value) {
           // 先只處理 <input> 元素
           if(value === undefined) {
               return this[0] && this[0].nodeName === 'INPUT' ?
                       this[0].value : null;
           }
           else {
               return XD.each(this, function(element) {
                   if(element.nodeName === 'INPUT') {
                       element.value = value;
                   }
               });
           }
       },
       append: function(childs) {
           if(typeof childs === 'string' || childs.nodeType) {
               childs = XD(childs);
           }

           if(this.length === 1) { // 只有一個父節點
               var parent = this[0];
               XD.each(childs, function(child) {
                   parent.appendChild(child);
               });
           }
           else if(this.length > 1){ // 有多個父節點
               XD.each(this, function(parent) {
                   childs.each(function(child) {
                       // 複製子節點
                       var container = document.createElement('div');
                       container.appendChild(child);
                       container.innerHTML = container.innerHTML;
                       parent.appendChild(container.firstChild);
                   });
               });
           }
           return this;
       },
       remove: function() { // 從父節點移除
           return XD.each(this, function(element) {
               element.parentNode.removeChild(element);
           });
       }

   };

each()方法可以讓你指定回呼函式,對收集的元素都會呼叫操作,每次呼叫時,回呼函式的第一個引數就是當時的DOM元素,而this也設為當時的DOM元素。例如:
XD('img').each(function(element) {
    var src1 = element.src;
    var src2 = this.src;
    ...
});

如果你有多個元素要設定其innerHTML,則可以如下:
XD('div').html('<b>文字</b>');

如果有多個div元素,則以上會將所有div元素的innerHTML都設為指定的文字。如果沒有指定文字,例如以下預設取得所收集元素的第一個div的innerHTML:
var html = XD('div').html();

類似的,attr()可用來設置取得取屬性:
XD('img').attr('src', 'caterpillar.jpg'); // 設定所有收集的img元素的src屬性
XD('img').attr('src'); // 取得所收集第一個img元素的src屬性

val()則可以讓你取得或設定<input>的value:
XD('input').val('default');  // 將所有<input>的value設定為'default'
XD('input').val();           // 取得第一個<input>的value

append()方法可以用來附加子節點,如果有多個父節點,則來源節點會進行複製。remove()方法可以將自己從父節點移除。例如:
XD('div').append('<img>'); // 建立<img>附加至所有的<div>
XD('div').remove();  // 移除所有<div>

像上面的attr()或html()方法,其實都是傳回收集物件本身(XD.each()第一個參數傳入this,最後XD.each()執行結束傳回的就是那個this),所以你可以如下進行鏈狀操作:
XD('<img>,<img>')
    .attr('src', 'images/caterpillar_small.jpg')
    .each(function(element) {
        // ... 作一些事
    });


在將來,若別人想要擴充這個程式庫,可以在他的.js中撰寫:
(function(XD) {
    XD.mth.get = function(index) {
        // this 參考至收集元素的物件
        return this[index];
    };
})(this.XD);

那麼他就可以這麼使用:
XD('img').get(0);  // 傳回第一個img元素

如果你曾經使用過 jQuery,對於以上的使用模式應該很眼熟,事實上,這邊的封裝與 建立核心公用函式 中的封裝,都是在模彷 jQuery 建立程式庫的方式,只不過這邊作了一定程度的簡化。就目前完成的.js,這邊命名為gossip-0.2.js,就在這邊找到完整內容:

如果你對jQuery的運作原理有興趣,這個簡化後的檔案可以作為開始,在之後的文件,也會適當地使用這個.js來簡化範例的撰寫。例如,可以稍微簡化一下 修改文件 中第一個範例
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<script type="text/javascript" src="js/gossip-0.2.js"></script>
<script type="text/javascript">
window.onload = function() {
XD('#add')[0].onclick = function() {
var imgXD = XD('<img>').attr('src', XD('#src').val());
imgXD[0].onclick = function() {
XD(this).remove();
};
XD('#images').append(imgXD);
};
};
</script>
</head>
<body>
<input id="src" type="text"><button id="add">新增圖片</button>
<div id="images"></div>
</body>
</html>


可以稍微簡化一下 修改文件 中第二個範例:
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<script type="text/javascript" src="js/gossip-0.2.js"></script>
<script type="text/javascript">
window.onload = function() {
var container1XD = XD('#container1');
var container2XD = XD('#container2');
XD('#image')[0].onclick = function() {
if(this.parentNode === container1XD[0]) {
container2XD.append(this);
}
else {
container1XD.append(this);
}
};
};
</script>
</head>
<body>
容器一:<div id="container1">
<img id="image" src=
"https://openhome.cc/Gossip/images/caterpillar_small.jpg"/>
</div><br>
容器二:<div id="container2"></div>
</body>
</html>

封裝還不完善,隨著文件的進行,之後還會逐步完善這個程式庫。