如何使用HTML5新特性Mutation Observer实现编辑器的撤销和回退操作

发布时间:2021-08-27 10:50:57 作者:小新
来源:亿速云 阅读:165

这篇文章主要介绍如何使用HTML5新特性Mutation Observer实现编辑器的撤销和回退操作,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

MutationObserver介绍

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

Mutation Observer(变动观察器)是监视DOM变动的接口。当DOM对象树发生任何变动时,Mutation Observer会得到通知。

Mutation Observer有以下特点:

 •它等待所有脚本任务完成后,才会运行,即采用异步方式
 •它把DOM变动记录封装成一个数组进行处理,而不是一条条地个别处理DOM变动。
 •它即可以观察发生在DOM节点的所有变动,也可以观察某一类变动

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实现的 undo 和 redo (撤销回退的管理)的管理插件 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;
   }
});

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上面运行完全正常:

代码如下:



DEMO在IE下的截图:

如何使用HTML5新特性Mutation Observer实现编辑器的撤销和回退操作

MutatoinObserver的浏览器兼容性:

FeatureChromeFirefox (Gecko)Internet ExplorerOperaSafari
Basic support

18

webkit

26

14(14)11156.0WebKit

以上是“如何使用HTML5新特性Mutation Observer实现编辑器的撤销和回退操作”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注亿速云行业资讯频道!

推荐阅读:
  1. GIT安装及详细使用
  2. vue3.0新特性是什么

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

html5 mutation

上一篇:如何通过构造AJAX参数实现表单元素JSON相互转换

下一篇:在ASP.NET 2.0中如何使用Repeater和DataList单页面实现主/从报表

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》