基于HTML5新特性Mutation Observer實現(xiàn)編輯器的撤銷和回退操作

字號:


    Mutation Observer(變動觀察器)是監(jiān)視DOM變動的接口。當DOM對象樹發(fā)生任何變動時,Mutation Observer會得到通知,本文給大家分享基于HTML5新特性Mutation Observer實現(xiàn)編輯器的撤銷和回退操作,感興趣的朋友參考下
    MutationObserver介紹
    MutationObserver給開發(fā)者們提供了一種能在某個范圍內(nèi)的DOM樹發(fā)生變化時作出適當反應的能力.該API設計用來替換掉在DOM3事件規(guī)范中引入的Mutation事件.
    Mutation Observer(變動觀察器)是監(jiān)視DOM變動的接口。當DOM對象樹發(fā)生任何變動時,Mutation Observer會得到通知。
    Mutation Observer有以下特點:
    •它等待所有腳本任務完成后,才會運行,即采用異步方式
    •它把DOM變動記錄封裝成一個數(shù)組進行處理,而不是一條條地個別處理DOM變動。
    •它即可以觀察發(fā)生在DOM節(jié)點的所有變動,也可以觀察某一類變動
    MDN的資料: MutationObserver
    MutationObserver是一個構造函數(shù), 所以創(chuàng)建的時候要通過 new MutationObserver;
    實例化MutationObserver的時候需要一個回調(diào)函數(shù),該回調(diào)函數(shù)會在指定的DOM節(jié)點(目標節(jié)點)發(fā)生變化時被調(diào)用,
    在調(diào)用時,觀察者對象會 傳給該函數(shù) 兩個參數(shù):
    1:第一個參數(shù)是個包含了若干個MutationRecord對象的數(shù)組;
    2:第二個參數(shù)則是這個觀察者對象本身.
    比如這樣:
    代碼如下:
    var observer = new MutationObserver(function(mutations) { 
    mutations.forEach(function(mutation) { 
    console.log(mutation.type); 
    }); 
    });
    observer的方法
    實例observer有三個方法: 1: observe  ;2: disconnect ; 3: takeRecords   ;
    observe方法
    observe方法:給當前觀察者對象注冊需要觀察的目標節(jié)點,在目標節(jié)點(還可以同時觀察其后代節(jié)點)發(fā)生DOM變化時收到通知;
    這個方法需要兩個參數(shù),第一個為目標節(jié)點, 第二個參數(shù)為需要監(jiān)聽變化的類型,是一個json對象,  實例如下:
    代碼如下:
    observer.observe( document.body, {
    'childList': true, //該元素的子元素新增或者刪除
    'subtree': true, //該元素的所有子元素新增或者刪除
    'attributes' : true, //監(jiān)聽屬性變化
    'characterData' : true, // 監(jiān)聽text或者comment變化
    'attributeOldValue' : true, //屬性原始值
    'characterDataOldValue' : true 
    });
    disconnect方法
    disconnect方法會停止觀察目標節(jié)點的屬性和節(jié)點變化, 直到下次重新調(diào)用observe方法;
    takeRecords
    清空 觀察者對象的 記錄隊列,并返回一個數(shù)組, 數(shù)組中包含Mutation事件對象;
    MutationObserver實現(xiàn)一個編輯器的redo和undo再適合不過了, 因為每次指定節(jié)點內(nèi)部發(fā)生的任何改變都會被記錄下來, 如果使用傳統(tǒng)的keydown或者keyup實現(xiàn)會有一些弊端,比如:
    1:失去滾動, 導致滾動位置不準確;
    2:失去焦點;
    ....
    用了幾小時的時間,寫了一個通過MutationObserver實現(xiàn)的 undo 和 redo (撤銷回退的管理)的管理插件 MutationJS ,   可以作為一個單獨的插件引入:(http://files.cnblogs.com/files/diligenceday/MutationJS.js):
    代碼如下:
    /**
    * @desc MutationJs, 使用了DOM3的新事件 MutationObserve; 通過監(jiān)聽指定節(jié)點元素, 監(jiān)聽內(nèi)部dom屬性或者dom節(jié)點的更改, 并執(zhí)行相應的回調(diào);
    * */
    window.nono = window.nono || {};
    /**
    * @desc
    * */
    nono.MutationJs = function( dom ) {
    //統(tǒng)一兼容問題
    var MutationObserver = this.MutationObserver = window.MutationObserver ||
    window.WebKitMutationObserver ||
    window.MozMutationObserver;
    //判斷瀏覽器是或否支持MutationObserver;
    this.mutationObserverSupport = !!MutationObserver;
    //默認監(jiān)聽子元素, 子元素的屬性, 屬性值的改變;
    this.options = {
    'childList': true,
    'subtree': true,
    'attributes' : true,
    'characterData' : true,
    'attributeOldValue' : true,
    'characterDataOldValue' : true
    };
    //這個保存了MutationObserve的實例;
    this.muta = {};
    //list這個變量保存了用戶的操作;
    this.list = [];
    //當前回退的索引
    this.index = 0;
    //如果沒有dom的話,就默認監(jiān)聽body;
    this.dom = dom|| document.documentElement.body || document.getElementsByTagName("body")[0];
    //馬上開始監(jiān)聽;
    this.observe( );
    };
    $.extend(nono.MutationJs.prototype, {
    //節(jié)點發(fā)生改變的回調(diào), 要把redo和undo都保存到list中;
    "callback" : function ( records , instance ) {
    //要把索引后面的給清空;
    this.list.splice( this.index+1 );
    var _this = this;
    records.map(function(record) {
    var target = record.target;
    console.log(record);
    //刪除元素或者是添加元素;
    if( record.type === "childList" ) {
    //如果是刪除元素;
    if(record.removedNodes.length !== 0) {
    //獲取元素的相對索引;
    var indexs = _this.getIndexs(target.children , record.removedNodes );
    _this.list.push({
    "undo" : function() {
    _this.disconnect();
    _this.addChildren(target, record.removedNodes ,indexs );
    _this.reObserve();
    },
    "redo" : function() {
    _this.disconnect();
    _this.removeChildren(target, record.removedNodes );
    _this.reObserve();
    }
    });
    //如果是添加元素;
    };
    if(record.addedNodes.length !== 0) {
    //獲取元素的相對索引;
    var indexs = _this.getIndexs(target.children , record.addedNodes );
    _this.list.push({
    "undo" : function() {
    _this.disconnect();
    _this.removeChildren(target, record.addedNodes );
    _this.reObserve();
    },
    "redo" : function () {
    _this.disconnect();
    _this.addChildren(target, record.addedNodes ,indexs);
    _this.reObserve();
    }
    });
    };
    //@desc characterData是什么鬼;
    //ref : http://baike.baidu.com/link?url=Z3Xr2y7zIF50bjXDFpSlQ0PiaUPVZhQJO7SaMCJXWHxD6loRcf_TVx1vsG74WUSZ_0-7wq4_oq0Ci-8ghUAG8a
    }else if( record.type === "characterData" ) {
    var oldValue = record.oldValue;
    var newValue = record.target.textContent //|| record.target.innerText, 不準備處理IE789的兼容,所以不用innerText了;
    _this.list.push({
    "undo" : function() {
    _this.disconnect();
    target.textContent = oldValue;
    _this.reObserve();
    },
    "redo" : function () {
    _this.disconnect();
    target.textContent = newValue;
    _this.reObserve();
    }
    });
    //如果是屬性變化的話style, dataset, attribute都是屬于attributes發(fā)生改變, 可以統(tǒng)一處理;
    }else if( record.type === "attributes" ) {
    var oldValue = record.oldValue;
    var newValue = record.target.getAttribute( record.attributeName );
    var attributeName = record.attributeName;
    _this.list.push({
    "undo" : function() {
    _this.disconnect();
    target.setAttribute(attributeName, oldValue);
    _this.reObserve();
    },
    "redo" : function () {
    _this.disconnect();
    target.setAttribute(attributeName, newValue);
    _this.reObserve();
    }
    });
    };
    });
    //重新設置索引;
    this.index = this.list.length-1;
    },
    "removeChildren" : function ( target, nodes ) {
    for(var i= 0, len= nodes.length; i<len; i++ ) {
    target.removeChild( nodes[i] );
    };
    },
    "addChildren" : function ( target, nodes ,indexs) {
    for(var i= 0, len= nodes.length; i<len; i++ ) {
    if(target.children[ indexs[i] ]) {
    target.insertBefore( nodes[i] , target.children[ indexs[i] ]) ;
    }else{
    target.appendChild( nodes[i] );
    };
    };
    },
    //快捷方法,用來判斷child在父元素的哪個節(jié)點上;
    "indexOf" : function ( target, obj ) {
    return Array.prototype.indexOf.call(target, obj)
    },
    "getIndexs" : function (target, objs) {
    var result = [];
    for(var i=0; i<objs.length; i++) {
    result.push( this.indexOf(target, objs[i]) );
    };
    return result;
    },
    /**
    * @desc 指定監(jiān)聽的對象
    * */
    "observe" : function( ) {
    if( this.dom.nodeType !== 1) return alert("參數(shù)不對,第一個參數(shù)應該為一個dom節(jié)點");
    this.muta = new this.MutationObserver( this.callback.bind(this) );
    //馬上開始監(jiān)聽;
    this.muta.observe( this.dom, this.options );
    },
    /**
    * @desc 重新開始監(jiān)聽;
    * */
    "reObserve" : function () {
    this.muta.observe( this.dom, this.options );
    },
    /**
    *@desc 不記錄dom操作, 所有在這個函數(shù)內(nèi)部的操作不會記錄到undo和redo的列表中;
    * */
    "without" : function ( fn ) {
    this.disconnect();
    fn&fn();
    this.reObserve();
    },
    /**
    * @desc 取消監(jiān)聽;
    * */
    "disconnect" : function () {
    return this.muta.disconnect();
    },
    /**
    * @desc 保存Mutation操作到list;
    * */
    "save" : function ( obj ) {
    if(!obj.undo)return alert("傳進來的第一個參數(shù)必須有undo方法才行");
    if(!obj.redo)return alert("傳進來的第一個參數(shù)必須有redo方法才行");
    this.list.push(obj);
    },
    /**
    * @desc ;
    * */
    "reset" : function () {
    //清空數(shù)組;
    this.list = [];
    this.index = 0;
    },
    /**
    * @desc 把指定index后面的操作刪除;
    * */
    "splice" : function ( index ) {
    this.list.splice( index );
    },
    /**
    * @desc 往回走, 取消回退
    * */
    "undo" : function () {
    if( this.canUndo() ) {
    this.list[this.index].undo();
    this.index--;
    };
    },
    /**
    * @desc 往前走, 重新操作
    * */
    "redo" : function () {
    if( this.canRedo() ) {
    this.index++;
    this.list[this.index].redo();
    };
    },
    /**
    * @desc 判斷是否可以撤銷操作
    * */
    "canUndo" : function () {
    return this.index !== -1;
    },
    /**
    * @desc 判斷是否可以重新操作;
    * */
    "canRedo" : function () {
    return this.list.length-1 !== this.index;
    }
    });
    MutationJS如何使用
    那么這個MutationJS如何使用呢?
    代碼如下:
    //這個是實例化一個MutationJS對象, 如果不傳參數(shù)默認監(jiān)聽body元素的變動;
    mu = new nono.MutationJs();
    //可以傳一個指定元素,比如這樣;
    mu = new nono.MutationJS( document.getElementById("div0") );
    //那么所有該元素下的元素變動都會被插件記錄下來;
    Mutation的實例mu有幾個方法:
    1:mu.undo()  操作回退;
    2:mu.redo()   撤銷回退;
    3:mu.canUndo() 是否可以操作回退, 返回值為true或者false;
    4:mu.canRedo() 是否可以撤銷回退, 返回值為true或者false;
    5:mu.reset() 清空所有的undo列表, 釋放空間;
    6:mu.without() 傳一個為函數(shù)的參數(shù), 所有在該函數(shù)內(nèi)部的dom操作, mu不做記錄;
    MutationJS實現(xiàn)了一個簡易的 undoManager 提供參考,在火狐和chrome,谷歌瀏覽器,IE11上面運行完全正常:
    代碼如下:
    <!DOCTYPE html>
    <html>
    <head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="http://cdn.bootcss.com/jquery/1.9.0/jquery.js"></script>
    <script src="http://files.cnblogs.com/files/diligenceday/MutationJS.js"></script>
    </head>
    <body>
    <div>
    <p>
    MutationObserver是為了替換掉原來Mutation Events的一系列事件, 瀏覽器會監(jiān)聽指定Element下所有元素的新增,刪除,替換等;
    </p>
    <div>
    <input type="button" value="撤銷操作" id="prev">;
    <input type="button" value="撤銷操作回退" id="next">;
    </div>
    <input type="button" value="添加節(jié)點" id="b0">;
    <input value="text" id="value">
    <div id="div"></div>
    </div>
    <script>
    window.onload = function () {
    window.mu = new nono.MutationJs();
    //取消監(jiān)聽
    mu.disconnect();
    //重新監(jiān)聽
    mu.reObserve();
    document.getElementById("b0").addEventListener("click", function ( ev ) {
    div = document.createElement("div");
    div.innerHTML = document.getElementById("value").value;
    document.getElementById("div").appendChild( div );
    });
    document.getElementById("prev").addEventListener("click", function ( ev ) {
    mu.undo();
    });
    document.getElementById("next").addEventListener("click", function ( ev ) {
    mu.redo();
    });
    };
    </script>
    </body>
    </html>