你的位置:首页 > Java教程

[Java教程]使用HTML5新特性Mutation Observer实现编辑器的撤销和撤销回退操作


   MutationObserver介绍

   MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

   MDN的资料:MutationObserver

 

  MutationObserver是一个构造函数, 所以创建的时候要通过 new MutationObserver;

  实例化MutationObserver的时候需要一个回调函数,该回调函数会在指定的DOM节点(目标节点)发生变化时被调用,

  在调用时,观察者对象会传给该函数两个参数:

    1:第一个参数是个包含了若干个MutationRecord对象的数组;    2:第二个参数则是这个观察者对象本身.

 

  比如这样:

    var observer = new MutationObserver(function(mutations) {       mutations.forEach(function(mutation) {         console.log(mutation.type);       });     });

  observer的方法

  实例observer有三个方法: 1: observe  ;2: disconnect ; 3: takeRecords   ;

  observe方法

  observe方法:给当前观察者对象注册需要观察的目标节点,在目标节点(还可以同时观察其后代节点)发生DOM变化时收到通知;

  这个方法需要两个参数,第一个为目标节点, 第二个参数为需要监听变化的类型,是一个json对象,  实例如下:

    observer.observe( document.body, {      'childList': true, //该元素的子元素新增或者删除      'subtree': true, //该元素的所有子元素新增或者删除      'attributes' : true, //监听属性变化      'characterData' : true, // 监听text或者comment变化      'attributeOldValue' : true, //属性原始值      'characterDataOldValue' : true     });

  disconnect方法

  disconnect方法会停止观察目标节点的属性和节点变化, 直到下次重新调用observe方法;

    takeRecords

  清空观察者对象的记录队列,并返回一个数组, 数组中包含Mutation事件对象;

 

  MutationObserver实现一个编辑器的redo和undo再适合不过了, 因为每次指定节点内部发生的任何改变都会被记录下来, 如果使用传统的keydown或者keyup实现会有一些弊端,比如:

1:失去滚动, 导致滚动位置不准确;2:失去焦点;....

 

  用了几小时的时间,写了一个通过MutationObserver实现的undoredo(撤销回退的管理)的管理插件MutationJS,  可以作为一个单独的插件引入:(http://files.cnblogs.com/files/diligenceday/MutationJS.js):

/** * @desc MutationJs, 使用了DOM3的新事件 MutationObserve; 通过监听指定节点元素, 监听内部dom属性或者dom节点的更改, 并执行相应的回调; * */window.nono = window.nono || {};/** * @desc * */nono.MutationJs = function( dom ) {  //统一兼容问题  var MutationObserver = this.MutationObserver = window.MutationObserver ||    window.WebKitMutationObserver ||    window.MozMutationObserver;  //判断浏览器是或否支持MutationObserver;  this.mutationObserverSupport = !!MutationObserver;  //默认监听子元素, 子元素的属性, 属性值的改变;  this.options = {    'childList': true,    'subtree': true,    'attributes' : true,    'characterData' : true,    'attributeOldValue' : true,    'characterDataOldValue' : true  };  //这个保存了MutationObserve的实例;  this.muta = {};  //list这个变量保存了用户的操作;  this.list = [];  //当前回退的索引  this.index = 0;  //如果没有dom的话,就默认监听body;  this.dom = dom|| document.documentElement.body || document.getElementsByTagName("body")[0];  //马上开始监听;  this.observe( );};$.extend(nono.MutationJs.prototype, {  //节点发生改变的回调, 要把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发生改变, 可以统一处理;      }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在父元素的哪个节点上;  "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 指定监听的对象   * */  "observe" : function( ) {    if( this.dom.nodeType !== 1) return alert("参数不对,第一个参数应该为一个dom节点");    this.muta = new this.MutationObserver( this.callback.bind(this) );    //马上开始监听;    this.muta.observe( this.dom, this.options );  },  /**   * @desc 重新开始监听;   * */  "reObserve" : function () {    this.muta.observe( this.dom, this.options );  },  /**   *@desc 不记录dom操作, 所有在这个函数内部的操作不会记录到undo和redo的列表中;   * */  "without" : function ( fn ) {    this.disconnect();    fn&fn();    this.reObserve();  },   /**   * @desc 取消监听;   * */   "disconnect" : function () {    return this.muta.disconnect();  },   /**   * @desc 保存Mutation操作到list;   * */  "save" : function ( obj ) {    if(!obj.undo)return alert("传进来的第一个参数必须有undo方法才行");    if(!obj.redo)return alert("传进来的第一个参数必须有redo方法才行");    this.list.push(obj);  },  /**   * @desc ;   * */  "reset" : function () {    //清空数组;    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;  }});

View Code

   

  MutationJS如何使用

  那么这个MutationJS如何使用呢?

//这个是实例化一个MutationJS对象, 如果不传参数默认监听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() 传一个为函数的参数, 所有在该函数内部的dom操作, mu不做记录;

  

  MutationJS实现了一个简易的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的一系列事件, 浏览器会监听指定Element下所有元素的新增,删除,替换等;    </p>    <div style="padding:20px;border:1px solid #f00">      <input type="button" value="撤销操作" id="prev">;      <input type="button" value="撤销操作回退" id="next">;    </div>    <input type="button" value="添加节点" id="b0">;    <input value="text" id="value">    <div id="div"></div>  </div><script>  window.onload = function () {    window.mu = new nono.MutationJs();    //取消监听    mu.disconnect();    //重新监听    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>

   DEMO在IE下的截图:

 MutatoinObserver的浏览器兼容性:

FeatureChromeFirefox (Gecko)Internet ExplorerOperaSafari
Basic support

18 webkit
26

14 (14)11156.0 WebKit

  阮一峰Mutationobserver

  MDN的资料:MutationObserver


作者: NONO  

出处:http://www.cnblogs.com/diligenceday/
QQ:287101329