首页 web前端 H5教程 基于HTML5新特性Mutation Observer实现编辑器的撤销和回退操作_html5教程技巧

基于HTML5新特性Mutation Observer实现编辑器的撤销和回退操作_html5教程技巧

May 16, 2016 pm 03:46 PM
撤销 编辑器

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 target.removeChild( nodes[i] );
};
},
"addChildren" : function ( target, nodes ,indexs) {
for(var i= 0, len= nodes.length; 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 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上面运行完全正常:


复制代码
代码如下:












MutationObserver是为了替换掉原来Mutation Events的一系列事件, 浏览器会监听指定Element下所有元素的新增,删除,替换等;



;
;

;



<script><br /> window.onload = function () {<br /> window.mu = new nono.MutationJs();<br /> //取消监听<br /> mu.disconnect();<br /> //重新监听<br /> mu.reObserve();<br /> document.getElementById("b0").addEventListener("click", function ( ev ) {<br /> div = document.createElement("div");<br /> div.innerHTML = document.getElementById("value").value;<br /> document.getElementById("div").appendChild( div );<br /> });<br /> document.getElementById("prev").addEventListener("click", function ( ev ) {<br /> mu.undo();<br /> });<br /> document.getElementById("next").addEventListener("click", function ( ev ) {<br /> mu.redo();<br /> });<br /> };<br /></script>


DEMO在IE下的截图:

MutatoinObserver的浏览器兼容性:

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support
18

webkit

26

14(14) 11 15 6.0WebKit

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1664
14
CakePHP 教程
1422
52
Laravel 教程
1317
25
PHP教程
1268
29
C# 教程
1242
24
如何解决 Windows 11 中的文件名或扩展名过长的问题? 如何解决 Windows 11 中的文件名或扩展名过长的问题? Apr 22, 2023 pm 04:37 PM

您在传输文件时是否遇到过任何问题,并且禁止您这样做?好吧,许多Windows用户最近报告说,他们在将文件复制并粘贴到文件夹中时遇到了问题,其中抛出了一个错误,提示“目标文件夹的文件名太长”。此外,其他一些Windows用户在打开任何文件时表示失望,并说“文件名或扩展名太长”,他们无法打开文件。这不允许他们将文件传输到任何其他文件夹,这让用户感到失望。在分析问题时,我们提出了一系列解决方案,可能有助于缓解问题,用户可以轻松传输文件。如果您也遇到类似情况,请参阅此帖子以了解更多信息。来源:https

15 款 Python 编辑器/ IDE 详细攻略,总有一款适合你! 15 款 Python 编辑器/ IDE 详细攻略,总有一款适合你! Aug 09, 2023 pm 05:44 PM

写 Python 代码最好的方式莫过于使用集成开发环境(IDE)了。它们不仅能使你的工作更加简单、更具逻辑性,还能够提升编程体验和效率。每个人都知道这一点。而问题在于,如何从众多选项中选择最好的 Python 开发环境。

如何在Windows 11、10中关闭Windows Defender智能屏幕? 如何在Windows 11、10中关闭Windows Defender智能屏幕? Apr 26, 2023 am 11:46 AM

许多Windows用户最近报告说,当WindowsDefenderSmartScreen警告用户不要启动MicrosoftWindows无法识别的应用程序时,他们感到恼火,他们每次都必须单击“仍然运行”选项。Windows用户不确定他们目前可以做些什么来避免或禁用它。在研究了这个问题后,我们发现系统上的WindowsDefender功能可以通过设置应用程序或本地组策略编辑器或通过调整注册表文件来禁用。通过这样做,用户将不再需要面对防守者SmartScreen。如果您的系统也遇到

C语言编程必备软件:五个推荐给初学者的好帮手 C语言编程必备软件:五个推荐给初学者的好帮手 Feb 20, 2024 pm 08:18 PM

C语言作为一门基础而重要的编程语言,对于初学者来说,选择合适的编程软件是非常重要的。在市场上有许多不同的C语言编程软件可供选择,但对于初学者来说,适合自己的选择可能有些困惑。本文将推荐给初学者的五个C语言编程软件,帮助他们快速入门和提高编程能力。Dev-C++Dev-C++是一款免费开源的集成开发环境(IDE),特别适合初学者使用。它简单易用,集成了编辑器、

修复 Windows 11/10 登录选项被禁用的问题 修复 Windows 11/10 登录选项被禁用的问题 May 07, 2023 pm 01:10 PM

许多Windows用户都遇到过由于登录尝试失败或多次关闭系统而无法登录Windows11/10系统的问题。用户很沮丧,因为他们对此无能为力。用户可能忘记了登录系统的PIN码,或者在使用或安装软件时出现卡顿,系统可能被多次强制关闭。因此,我们制定了一份最好的可用解决方案列表,这些解决方案无疑将帮助消费者解决这个问题。要了解更多信息,请继续阅读本文。注意:在此之前,请确保您拥有系统的管理员凭据和Microsoft帐户密码以重置PIN。如果没有,请等待一个小时左右,然后尝试使用正确的PIN

如何使用 ClipChamp:免费的 Windows 11 视频编辑器 如何使用 ClipChamp:免费的 Windows 11 视频编辑器 Apr 20, 2023 am 11:55 AM

还记得Windows7上的WindowsMovieMaker吗?自从停止WindowsMovieMaker以来,微软还没有推出任何真正的电影制作者。另一方面,他们尝试用一个小巧轻便的内置视频编辑器来改造照片应用程序。很长一段时间后,微软推出了Clipchamp,这是一款适用于所有Windows11设备的更好的视频处理器。在本文中,我们将深入探讨如何从Windows11设备上的Clipchamp应用程序中获取所有内容。如何使用Clipchamp–详细教程我们提供

如何在 Windows 11 和 10 上使用 Clipchamp 视频编辑器 如何在 Windows 11 和 10 上使用 Clipchamp 视频编辑器 Apr 17, 2023 pm 07:55 PM

如何在Windows上安装和使用ClipchampClipchamp应用程序尚未预装在Windows上,但这是未来的计划。同时,您需要先下载并安装Clipchamp。要在Windows11和Windows10上安装和使用Clipchamp:从MicrosoftStore下载并安装Clipchamp。安装后,在开始菜单中搜索Clipchamp以启动它。在Clipchamp窗口中,您需要使用您的Microsoft或Google帐户登录,或者使用您自己的个人电子邮

在每个 Word 编辑器中使用的 10 个删除线快捷方式 在每个 Word 编辑器中使用的 10 个删除线快捷方式 Apr 16, 2023 pm 05:25 PM

文字编辑器,也称为文字处理器,可以定义为允许您创建、打印和编辑文档的设备或软件。您可以键入内容、将其显示在屏幕或打印材料上、以电子方式存储,并使用不同的键盘快捷键、字符和命令从键盘进行修改,包括用于删除线的键盘快捷键。计算机的制造是为了帮助解决不同的问题。但是,文字处理是他们帮助的最受欢迎的功能。由于技术进步,您可以将文字编辑器作为安装在移动设备和计算机上的软件应用程序或作为不同供应商提供的云服务访问。文字处理器于1960年代初作为类似于电动打字机的独立机器首次推出。它们比打字机更好,因为它们允

See all articles