使用HTML5新特性Mutation Observer实现编辑器的撤销和撤销回退操作
MutationObserver介绍
MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.
MDN的资料:MutationObserver
MutationObserver是一个构造函数, 所以创建的时候要通过 new MutationObserver;
实例化MutationObserver的时候需要一个回调函数,该回调函数会在指定的DOM节点(目标节点)发生变化时被调用,
在调用时,观察者对象会传给该函数两个参数:
:第一个参数是个包含了若干个MutationRecord对象的数组; :第二个参数则是这个观察者对象本身.
比如这样:
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实现会有一些弊端,比如:
:失去滚动, 导致滚动位置不准确;
:失去焦点;
....
用了几小时的时间,写了一个通过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上面运行完全正常:
<!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的浏览器兼容性:
Feature | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support |
18 webkit |
14 (14) | 11 | 15 | 6.0 WebKit |
MDN的资料:MutationObserver
作者: NONO
出处:http://www.cnblogs.com/diligenceday/
QQ:287101329
使用HTML5新特性Mutation Observer实现编辑器的撤销和撤销回退操作的更多相关文章
- 转: HTML5新特性之Mutation Observer
转: HTML5新特性之Mutation Observer Mutation Observer是什么 Mutation Observer(变动观察器)是监视DOM变动的接口.当DOM对象树发生任何变动 ...
- html5新特性
这一篇博文不会告诉你怎么去使用html5的新特性,只会给你总结一下新特性------对于好学的人可以把这篇文章当做一个目录 对于初接触的人来说是一个导向 对于已经接触过的人来说是一个检测你掌握程度的检 ...
- HTML5新特性之CSS+HTML5实例
1.新的DOCTYPE和字符集 HTML5的一项准则就是化繁为简,Web页面的DOCTYPE被极大的简化. <!DOCTYPE html> 同时字符集声明也被简化了: <meta c ...
- HTML5新特性:FileReader 和 FormData
连接在这里: HTML5新特性:FileReader 和 FormData
- web全栈架构师[笔记] — 03 html5新特性
HTML5新特性 一.geolocation PC端 精度比较低 通过IP库定位 移动端 通过GPS window.navigator.geolocation 单次 getCurrentPositio ...
- HTML5新特性:范围样式
原文出处:http://blog.csdn.net/hfahe/article/details/7381141 Chromium 最近实现了一个HTML5的新特性:范围样式,又叫做< ...
- html5新特性与用法大全了解一下
有好多小伙伴私聊我问我html5新特性 和用法,下面我给大家具体介绍一下html5都新加了哪些新特性,下面我给大家总结一下. 1)新的语义标签 footer header 等等2)增强型表单 表单2. ...
- html5新特性contenteditable 属性更容易实现动态表单
介绍html5新特性的一个属性:contenteditable 作用域全局.所有的块标签都可以,例如:span.p.div.td等标签.但是,不可以作用域<br/>类型的标签. conte ...
- HTML5新特性之文件和二进制数据的操作 Blob对象
HTML5新特性之文件和二进制数据的操作 1.Blob对象 2.FileList对象 3.File对象 4.FileReader 对象 5.URL对象
随机推荐
- poj2486Apple Tree[树形背包!!!]
Apple Tree Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 9989 Accepted: 3324 Descri ...
- KSFramework配置表:扩展表格解析类型
解析和扩展表格 配置表示例 配置表模块在编译时,把Excel转化成TSV,并根据Excel的头部信息,生成对应的代码: 比如源码库中的Test.xlsx Excel文件,两个列头,Id和Value,其 ...
- 利用 Process Monitor 找出某个 Windows 选项所对应的注册表值
多 时候我们要调整一项 Windows 的功能时只需更改一下注册表即可实现.而很多大家眼中所谓的高手,对 Windows 注册表更是玩得出神入化.难道这些高手把 Windows 注册表都记下来了?答案 ...
- Noip2000 T3 单词接龙
题目描述 单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”(每个单词都最多在“龙”中出现两次),在两个单词相连时,其重合 ...
- 理解android.intent.action.MAIN 与 android.intent.category.LAUNCHER
刚才看了一下sundy的视频<LLY110426_Android应用程序启动>,里面讲到luncher这个activity通过获取应用程序信息来加载应用程序,显示给用户,其中就是通过一个应 ...
- tomcat相关配置技巧梳理
tomcat常用架构:1)nginx+tomcat:即前端放一台nginx,然后通过nginx反向代理到tomcat端口(可参考:分享一例测试环境下nginx+tomcat的视频业务部署记录)2)to ...
- 交叉验证 Cross validation
来源:CSDN: boat_lee 简单交叉验证 hold-out cross validation 从全部训练数据S中随机选择s个样例作为训练集training set,剩余的作为测试集testin ...
- Gruntjs: grunt-contrib-jst
预编译Underscore模板到JST文件(Underscore:JS工具库) generate JavaScript template functions Gruntfile的配置实例: modul ...
- velocity模板引擎学习(2)-velocity tools 2.0
使用velocity后,原来的很多标签无法使用了,必须借助velocity tools来完成,目前velocity tools最新版本是2.0,下面是velocity tools的一些注意事项: 1. ...
- Dapper Vs Dbentry
公司项目数据库访问采用的dapper,以前没有用过.今天简单的测试下了,dapper和dbentry 查询效率情况. public ActionResult Test() { Sys_UserFaca ...