zoukankan      html  css  js  c++  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) 11 15 6.0 WebKit

      阮一峰Mutationobserver

      MDN的资料:MutationObserver


    作者:
    NONO  

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

  • 相关阅读:
    Codeforces Round #481 (Div. 3) D. Almost Arithmetic Progression
    Codeforces Round #481 (Div. 3) G. Petya's Exams
    使用create-react-app 搭建react + ts + antd框架
    callback、promise和async、await的使用方法
    JS数组中Array.of()方法的使用
    react中替换关键字并且高亮显示的方法
    封装 jsonp请求数据的方法
    React的新特性 ---- Hooks ---- 的基本使用
    在canvas中使用其他HTML元素
    React的性能优化
  • 原文地址:https://www.cnblogs.com/diligenceday/p/5022346.html
Copyright © 2011-2022 走看看