
; (function ($) {

	/**
	* @author kevin Lv
	* @constructor touchNote
	* @description 划线批注
	* @see The <a href="#">kevin</a>
	* @example
	* 	对页面内容划线，并对划线部分进行批注
	*	$("#div").touchNote();
	*   带参数的用法 function中event为事件，root为根元素，ele调用元素，options为配置参数。
	*	$("#div").touchNote({
			hlClsName:'highligh_my',
			mouseover:function(event, root, ele, options){
				alert(ele.val());
			}
		});
	*   
	* @since version 1.0
	*/
	var winHeight = $(window).height();
	// 改变窗口大小
	$(window).on("resize.touchnote", function (e) {
		winHeight = $(window).height();
		console.log('winHeight:' + winHeight);
	});
	touchNote = {
		root: document.body,
		SplitType: { none: 'none', head: 'head', tail: 'tail', both: 'both' },
		OPERATE_Type: { ADD: 'add', UPDATE: 'update' },
		SelectedNodeType: { text: 'text', span: 'span' },
		TOUCHNOTE_INDEX: 'tn-index',
		DATA_ID: 'highlight-id',
		DATA_ID_EXTRA: 'highlight-id-extra',
		TMP_DATA_ID: 'tmp-highlight-id',
		DATA_SPLIT_TYPE: 'highlight-split-type',
		CAMEL_DATA_ID: 'highlightId',
		CAMEL_DATA_ID_EXTRA: 'highlightIdExtra',
		CAMEL_TMP_DATA_ID: 'tmpHighlightId',
		CAMEL_DATA_SPLIT_TYPE: 'highlightSplitType',
		ROOT_IDX: -2,
		UNKNOWN_IDX: -1,
		ID_DIVISION: ';',
		IS_MARK: true, // 是否可以标记
		// 获取
		getSelection: function (root) {
			console.log('带参数的用法 function中event为事件，root为根元素，ele调用元素，options为配置参数。', root);
			root = root || this.root;
			var selection = null;
			if (window.getSelection) {
				selection = window.getSelection();
			} else if (document.selection) {
				selection = document.selection;
			}
			var rangeOri = window.getSelection().getRangeAt(0);

			var range = rangeOri.cloneRange();//复制rang对象


			var start = { node: range.startContainer, offset: range.startOffset };
			var end = { node: range.endContainer, offset: range.endOffset };

			// 获取选区内的文本节点

			var nodes = getSelectedNodes(root, start, end);

			var text = this.getSelectionText();
			var htmltext = getSelectHtmlText(nodes);

			return new Selection(selection, range, nodes, text, htmltext);

		},
		getHighlightSource: function (root, selection, parSelectors) {
			root = root || this.root;
			var range = selection.range;
			var start = { node: range.startContainer, offset: range.startOffset };
			var end = { node: range.endContainer, offset: range.endOffset };
			var range = selection.range;
			var text = selection.text;
			var highlightSource = this.serialize(root, start, end, text, "tn-" + new Date().getTime(), parSelectors);
			return highlightSource;
		},
		serializeSelection: function (root, selection, parSelectors) {
			var highlightSource = this.getHighlightSource(root, selection, parSelectors);
			return this.serialize2Str(highlightSource);;
		},
		// 序列化来 root: Document | HTMLElement, hooks: HookMap
		serialize: function (root, start, end, text, id, parSelectors) {
			root = root || this.root;
			var startMeta = getDomMeta(start.node, start.offset, root, parSelectors);
			var endMeta = getDomMeta(end.node, end.offset, root, parSelectors);
			var highlightSource = new HighlightSource(startMeta, endMeta, text, id);
			return highlightSource;
		},
		serialize2Str: function (highlightSource) {
			var cloneJsonObj = $.extend(true, {}, highlightSource);//对象克隆
			//delete cloneJsonObj['create'];
			//delete cloneJsonObj['deSerialize'];
			var dataStr = JSON.stringify(cloneJsonObj);
			return dataStr;
		},
		deSerializefromStr: function (root, datastr) {
			root = root || this.root;
			var highlightSource = $.extend(true, new HighlightSource(), jQuery.parseJSON(datastr));//对象克隆					
			var range = highlightSource.deSerialize(root);
		},
		deSerializefromhs: function (root, highlightSource) {
			root = root || this.root;
			var range = highlightSource.deSerialize(root);
		},
		// 判读节点是否高亮包裹 node: HTMLElement : boolean
		isHighlightWrapNode: function (node) {
			return !!node.dataset && !!node.dataset[touchNote.CAMEL_DATA_ID];
		},
		// 获取选中的文本
		getSelectionText: function () {
			var text = '';
			if (window.getSelection) {
				text = window.getSelection().toString();
			} else if (document.selection && document.selection.type != "Control") {
				text = document.selection.createRange().text;
			}
			return text;
		},
		// 获取选中的文本 高亮包裹
		// selected{SelectedNode}, range{HighlightRange}, className{string[] | string}, wrapTag{string} return HTMLElement
		wrapHighlight: function (selected, range, className, wrapTag) {
			var parentNode = selected.node.parentNode;
			var prevNode = selected.node.previousSibling;
			var nextNode = selected.node.nextSibling;

			var wrapNode; //: HTMLElement;

			/* 在处理时，将需要包裹的各个文本片段（Text Node）分为三类情况：

			1、完全未被高亮包裹，则直接包裹该部分。
			2、属于被高亮包裹过的文本节点的一部分，则使用.splitText()将其拆分。
			3、是一段完全被高亮包裹的文本段，只更新id */

			// 没有包裹高亮则进行包裹
			if (!touchNote.isHighlightWrapNode(parentNode)) {
				wrapNode = wrapNewNode(selected, range, className, wrapTag);
			}
			// 部分高亮包裹，拆分
			else if (touchNote.isHighlightWrapNode(parentNode) && (!isEmptyNode(prevNode) || !isEmptyNode(nextNode))) {
				wrapNode = wrapPartialNode(selected, range, className, wrapTag);
			}
			// 剩下的是已经高亮包裹的，值增加extra id
			else {
				wrapNode = wrapOverlapNode(selected, range, className);
			}

			return wrapNode;
		},
		// 移除指定id的高亮，如果对应id的高亮不存在，则返回false
		removeHighlight: function (rootEle, id, callback) {
			console.log(id)
			// whether extra ids contains the target id id; 或者;id 分号可以没有
			var reg = new RegExp('(' + id + touchNote.ID_DIVISION + '|' + touchNote.ID_DIVISION + '?' + id + '$)'); // data中highlightIdExtra的正则

			var wrapTag = rootEle.getOptions('hltagName') //'span'; //高亮包裹元素
			var spans = rootEle.find(wrapTag + '[data-' + touchNote.DATA_ID + ']'); // 获取所有高亮元素


			// 移除的元素
			var toRemoves = [];
			// 更新主id和extraid的元素
			var idToUpdates = [];
			// 更新extra id的元素
			var extraToUpdates = [];

			$.each(spans, function (i, n) {
				var spanId = n.dataset[touchNote.CAMEL_DATA_ID]; // highlightId
				var spanExtraIds = n.dataset[touchNote.CAMEL_DATA_ID_EXTRA]; //highlightIdExtra


				// id相等并且没有extraid -> 移除
				if (spanId === id && !spanExtraIds) {
					toRemoves.push(n);
				}
				// id相等并且但是存在extraid -> 更新主id和extraid
				else if (spanId === id) {
					console.log(spanExtraIds)
					idToUpdates.push(n);
				}
				// id不相等但是extraid包含 -> 更新extra id
				else if (spanId !== id && reg.test(spanExtraIds)) {
					extraToUpdates.push(n);
				}
			});
			console.log(toRemoves)
			console.log(idToUpdates)
			console.log(extraToUpdates)
			// 移除
			$.each(toRemoves, function (i, n) {
				var parent = n.parentNode;
				var fr = document.createDocumentFragment();

				$.each(n.childNodes, function (i, c) {
					fr.appendChild(c.cloneNode(false));
				});

				var prev = n.previousSibling;
				var next = n.nextSibling;

				parent.replaceChild(fr, n);
				// 合并相邻的文本节点
				normalizeSiblingText(prev, true);
				normalizeSiblingText(next, false);
				//hooks.Remove.UpdateNodes.call(id, n, 'remove');
			});

			$.each(idToUpdates, function (i, n) {
				var dataset = n.dataset;
				var ids = dataset[touchNote.CAMEL_DATA_ID_EXTRA].split(touchNote.ID_DIVISION);
				var newId = ids.shift(); // 把数组的第一个元素从其中删除,并返回第一个元素的值

				// 寻找对应extraId对应的元素
				var overlapSpanEle = rootEle.find(wrapTag + '[data-' + touchNote.DATA_ID + '="' + newId + '"]');

				if (overlapSpanEle.length) {
					// 移除样式
					removeAllClass(n);
					// 添加样式 retain the class list of the overlapped wrapper which associated with "extra id"
					var clsList = overlapSpanEle[0].classList;
					console.log(clsList)
					for (var index = 0; index < clsList.length; index++) {
						$(n).addClass(clsList.item(index));
					}

					var nid = overlapSpanEle.data("nid"); // nid更新
					var username = overlapSpanEle.data("username");
					var createtime = overlapSpanEle.data("createtime");

					$(n).data("nid", nid);
					$(n).attr("data-nid", nid);
					$(n).data("username", username);
					$(n).attr("data-username", username);
					$(n).data("createtime", createtime);
					$(n).attr("data-createtime", createtime);
				} else { // 从批注区找
					var noteItemClsName = rootEle.getOptions('noteItemClsName');
					var noteEle = $("." + noteItemClsName + "[data-keyid='" + newId + "']");
					var nid = noteEle.data("nid");

					var username = noteEle.data("username");
					var createtime = noteEle.data("createtime");
					$(n).data("nid", nid);
					$(n).attr("data-nid", nid);
					$(n).data("username", username);
					$(n).attr("data-username", username);
					$(n).data("createtime", createtime);
					$(n).attr("data-createtime", createtime);
				}

				var idsv = ids.join(touchNote.ID_DIVISION); // highlightIdExtra，更新extraid
				$(n).data(touchNote.CAMEL_DATA_ID, newId);
				$(n).attr("data-" + touchNote.DATA_ID, newId); // highlightId 更新主id
				$(n).data(touchNote.CAMEL_DATA_ID_EXTRA, idsv);
				$(n).attr("data-" + touchNote.DATA_ID_EXTRA, idsv);
			});
			// 去掉extraId中的id
			$.each(extraToUpdates, function (i, n) {
				var extraIds = n.dataset[touchNote.CAMEL_DATA_ID_EXTRA];

				var extraIdv = extraIds.replace(reg, '');
				// n.dataset[touchNote.CAMEL_DATA_ID_EXTRA] = extraIdv; // highlightIdExtra
				$(n).data(touchNote.CAMEL_DATA_ID_EXTRA, extraIdv);
				$(n).attr("data-" + touchNote.DATA_ID_EXTRA, extraIdv); // highlightId 更新主id

			});
			if (callback && $.isFunction(callback)) {
				callback(toRemoves, idToUpdates, extraToUpdates);
			}
			return toRemoves.length + idToUpdates.length + extraToUpdates.length !== 0;
		},
		// 移除所有的高亮
		removeAllHighlight: function (rootEle, callback) {
			var spans = getHighlightsByRoot(rootEle, rootEle.getOptions('hltagName')); //获取根元素下的所有高亮元素,return jQueryObj

			spans.each(function (i, n) {
				var parent = n.parentNode;
				var fr = document.createDocumentFragment();

				$.each(n.childNodes, function (i, c) { //循环子节点
					fr.appendChild(c.cloneNode(false)); //浅克隆=标签里的元素，属性、事件没有复制。
				});
				parent.replaceChild(fr, n); // 将高亮用新生成的元素替换
			});
			if (callback && $.isFunction(callback)) {
				callback(spans);
			}
		}
	}
	// 选区对象
	function Selection(selection, range, selectedNodes, text, htmltext) {
		this.selection = selection;
		this.range = {
			startContainer: range.startContainer,
			startOffset: range.startOffset,
			endContainer: range.endContainer,
			endOffset: range.endOffset,
			domRect: range.getBoundingClientRect()
		}

		this.selectedNodes = selectedNodes;
		this.text = text;
		this.htmltext = htmltext;
	}
	// 源码对象
	function HighlightSource(startMeta, endMeta, text, id) {
		this.startMeta = startMeta; // DomMeta
		this.endMeta = endMeta; // DomMeta
		this.text = text; // string
		this.id = id; // string		
	}
	// 反序列化
	HighlightSource.prototype.deSerialize = function (root) {
		var { start, end } = queryElementNode(this, root);
		if (!start && !end) { //不存在则返回null
			return null;
		}
		var startInfo = getTextChildByOffset(start, this.startMeta.textOffset);
		var endInfo = getTextChildByOffset(end, this.endMeta.textOffset);
		var range = new HighlightRange(startInfo, endInfo, this.text, this.id, true);
		return range;
	}
	// 高亮处理 highlighter{Highlighter}
	HighlightSource.prototype.highlight = function (root, highlighter) {
		var range = this.deSerialize(root);
		var selectedNodes = getSelectedNodes(root, range.start, range.end);

		var nodes = [];
		$.each(selectedNodes, function (i, n) {
			var node = touchNote.wrapHighlight(n, range, highlighter.className, highlighter.tagName);
			nodes.push(node);
		});
		return nodes;
	}
	// 高亮对象
	function Highlighter(className, tagName) {
		this.className = className; // DomMeta
		this.tagName = tagName; // DomMeta
	}
	// 选中范围对象，用于与原生range转换
	function HighlightRange(start, end, text, id) {
		if ((start && start.node.nodeType !== 3) || (end && end.node.nodeType !== 3)) {
			console.log("类型错误！")
		}
		if (start) {
			this.start = formatDomNode(start); // DomMeta
		}
		if (end) {
			this.end = formatDomNode(end); // DomMeta
		}

		this.text = text; // string
		this.id = id; // string
	}
	// 获取根元素下的所有高亮元素,return jQueryObj
	function getHighlightsByRoot(rootEles, wrapTag) {

		var wraps;

		rootEles.each(function (i, n) {
			var list = $(n).find(wrapTag + '[data-' + touchNote.DATA_ID + ']');
			if (!wraps) {
				wraps = list;
			} else {
				wraps = wraps.add(list);
			}
		});

		return wraps;
	}
	/**
	 * 合并相邻的文本节点
	 * .normalize() API 在ie11下有bug
	 */
	function normalizeSiblingText(s, isNext) {
		if (!s || s.nodeType !== 3) {
			return;
		}

		var sibling = isNext ? s.nextSibling : s.previousSibling;

		if (sibling.nodeType !== 3) {
			return;
		}

		var text = sibling.nodeValue;

		s.nodeValue = isNext ? s.nodeValue + text : text + s.nodeValue;
		sibling.parentNode.removeChild(sibling);
	}
	// 获取配置
	$.fn.getOptions = function (optionName) {
		var eles = $(this); //表单
		if (optionName) {
			return eles.data("options")[optionName];
		}
		return eles.data("options");
	}
	// 获取选中选区对象
	$.fn.getSelection = function () {
		var selection = $(this).data('selection'); //data中取
		return selection;
	}
	// 根据选中对象增加高亮处理，并增加到批注区
	$.fn.addHighlighter = function (remarks, notesData,type) {
		console.log("🚀 ~ remarks, notesData:", remarks, notesData)
		var eles = $(this); //表单
		var options = eles.getOptions();
		var noteClassName = options.hlClsName; // 批注样式
		var root = options.root;

		// 移除临时高亮,防止干扰
		removeTmpHighlight(options.hltmpClsName);

		var selection = eles.getSelection(); //获取selection

		// 获取批注，并处理赋值
		var highlighter = new Highlighter(noteClassName, options.hltagName);
		// 根据数据处理显示，高亮，并增加悬停提示

		var highlightSource = touchNote.getHighlightSource(root, selection, options.parSelectors);

		if (highlightSource.startMeta) {
			var noteClassInitClsName = options.hlIntClsName; //高亮已经初始化的样式
			// 高亮,可能存在多个高亮元素
			var nodes = highlightSource.highlight(root, highlighter);
			// 处理提示内容
			var data = { 'username': options.username, 'createtime': new Date() };
			if (notesData) {
				data = {
					'username': options.username, 'createtime': new Date(), ...notesData
				}
			} else {
				data.keyid = highlightSource.id;
				data.keycode = highlightSource;
				data.remarks = remarks; //批注信息
			}


			// 添加到批注区
			var i = $(options.noteContainerSelector).find("." + options.noteItemClsName).length;
			var noteEle = options.addNoteEle(data, i, null, $(options.noteContainerSelector), root, eles, options);
			// 更新批注总数 count, datas, root, options
			options.updateNotesCount(i + 1, null, root, eles, options);
			// 处理高亮
			$.each(nodes, function (i, n) {
				var nodeEle = $(n);
				var hid = nodeEle.data(touchNote.CAMEL_DATA_ID);
				var tipsClass = 'codetips_' + data.keyid || hid;
				console.log(tipsClass)
				var tipsContentClass = 'content_' + tipsClass;

				if (!nodeEle.hasClass(noteClassInitClsName)) {

					// 调用处理方法
					eles.triggerHandler("dealCodeNoteEle", [nodeEle, data, tipsClass, tipsContentClass]); //添加样式
				}
				// 高亮处理完成后的处理方法，可再自己的实现中扩展
				if(type=='bookmark'){

				}else{
					options.highlightNoteAfter(nodeEle, noteEle, data, tipsClass, tipsContentClass, eles, options);
				}

			});


			return nodes;
		}
		return null;

	}

	$.fn.touchNote = function (options) {
		debug("选择器[" + this.selector + "]调用自定义插件 touchNote，调用元素数 " + this.length + " 个[jQuery " + this.jquery + "]");
		/**
		* @description {Json} 设置参数选项
		* @field
		*/
		var options = $.extend({}, $.fn.touchNote.defaults, options);
		var noteClassName = options.hlClsName; // 批注样式
		var noteClassInitClsName = options.hlIntClsName; //高亮已经初始化的样式

		var hldatas = options.hldatas;
		var root = options.root;
		touchNote.root = root;
		/**
		* @description 处理函数
		*/
		var eles = $(this); //表单
		eles.each(function (i, n) {
			var $this = $(this);
			$this.attr('data-' + touchNote.TOUCHNOTE_INDEX, i);
			$this.data('options', options);

			// 处理父节点
			$this.off("dealCodeNoteEle").on("dealCodeNoteEle", function (event, ele, data, tipsClass, tipsContentClass) {

				var remarks = data.remarks; // 内容
				var keycode = data.keycode;
				var username = data.username;
				var createtime = data.createtime;
				ele.addClass(noteClassInitClsName).addClass(tipsClass).data('obj', data); //添加样式				
				addDatas(ele, data);

				// 高亮选区鼠标移入移出的处理
				ele.off("mouseenter.touchnote").on("mouseenter.touchnote", function (event) {
					var e = event || window.event;
					options.highlightNoteMouseover[0](e, root, ele, options);
				},);
				ele.off("mouseleave.touchnote").on("mouseleave.touchnote", function (event) {
					var e = event || window.event;
					options.highlightNoteMouseover[1](e, root, ele, options);
				});
			});

			// 获取批注，并处理赋值
			var highlighter = new Highlighter(noteClassName, options.hltagName);
			// 根据数据处理显示，高亮，并增加悬停提示
			$.each(hldatas, function (i, n) {
				var data = n;
				var nid = data.nid;
				var keycode = data.keycode;

				// 添加到批注区
				var noteEle = options.addNoteEle(data, i, hldatas, $(options.noteContainerSelector), root, $this, options);

				try {
					var highlightSource = $.extend(true, new HighlightSource(), keycode);//对象克隆					

					if (highlightSource.startMeta) {
						// 获取高亮元素
						highlightSource.highlight(root, highlighter);

						// 处理提示内容
						var hid = keycode.id;
						var tipsClass = 'codetips_' + hid;
						console.log(tipsClass)
						var tipsContentClass = 'content_' + tipsClass;
						// 提示元素创建
						var codeNodeEles = $('.' + noteClassName + "[data-" + touchNote.DATA_ID + "='" + hid + "']");
						if (codeNodeEles.length > 0) {
							codeNodeEles.each(function () {
								var nodeEle = $(this);
								if (!nodeEle.hasClass(noteClassInitClsName)) {
									$this.triggerHandler("dealCodeNoteEle", [nodeEle, data, tipsClass, tipsContentClass]); //添加样式
								}
								// 高亮处理完成后的处理方法，可再自己的实现中扩展
								options.highlightNoteAfter(nodeEle, noteEle, data, tipsClass, tipsContentClass, $this, options);
							});
						}

					}

				} catch (e) { console.log(e); }


			});
			// 更新批注总数 count, datas, root, options
			options.updateNotesCount(hldatas.length, hldatas, root, $this, options);
			// 批注区更新数目
			$(options.noteContainerSelector).off("refreshCount").on("refreshCount", function () { //删除					
				// 更新批注总数 count, datas, root, options
				options.refreshNotesCount($(this), root, $this, options);
			});

			// 监听释放鼠标按钮事件
			$this.off("mouseup.touchnote").on("mouseup.touchnote", function (event) {
				var e = event || window.event;
				options.mouseup(e, root, $this, options);
			});

			// 监听释放鼠标划过事件
			$this.off("mouseover.touchnote").on("mouseover.touchnote", function (event) {
				var e = event || window.event;
				options.mouseover(e, root, $this, options);
			});

			// 监听移动端触摸事件
			$this.off("touchstart.touchnote").on("touchstart.touchnote", function (event) {
				var e = event || window.event;
				setTimeout(() => {
					options.mouseup(e, root, $this, options);
				}, 800)
			});

		});
		// 取消无效的高亮显示
		$(document).off("click.hl.touchnote").on("click.hl.touchnote", function (event) {
			var targetE = $(event.target);

			$("." + noteClassName).not('.' + noteClassInitClsName).removeClass(noteClassName);
			$(".note-dialog").hide(); //弹出框隐藏

		});


	};
	//私有方法
	/**
	* @description 输出选中对象的个数到控制台
	* @param {msg} String 输出内容
	*/
	function debug(msg) {
		if (window.console && window.console.log) {
			window.console.log('touchnote.js log: ' + msg);
		}
	}

	/**
	 * 如果开始节点结束节点相同，不需要遍历树.
	 */
	// startNode: Text, startOffset: number, endOffset: number
	var getNodesIfSameStartEnd = function (startNode, startOffset, endOffset) {
		var element = startNode;

		while (element) {
			element = element.parentNode;
		}

		startNode.splitText(startOffset);
		var passedNode = startNode.nextSibling; // Text
		passedNode.splitText(endOffset - startOffset);
		return [{
			node: passedNode,
			type: touchNote.SelectedNodeType.text,
			splitType: touchNote.SplitType.both
		}];
	};

	// root: Document | HTMLElement, start: DomNode, end: DomNode
	// 获取选中节点 返回示例：{node: node, type: touchNote.SelectedNodeType.text, splitType: touchNote.SplitType.head}
	var getSelectedNodes = function (root, start, end) {
		var startNode = start.node;
		var endNode = end.node;
		var startOffset = start.offset;
		var endOffset = end.offset;

		// 如果开始节点和结束节点相同 拆分当前节点
		if (startNode === endNode && startNode instanceof Text) {
			return getNodesIfSameStartEnd(startNode, startOffset, endOffset);
		}

		var nodeStack = [root];
		var selectedNodes = [];

		var withinSelectedRange = false;
		var curNode = null;
		// 遍历dom树
		while ((curNode = nodeStack.pop())) {

			var children = curNode.childNodes;

			for (var i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}

			// 只选择Text节点，即文本节点
			if (curNode === startNode) {
				if (curNode.nodeType === 3) {
					curNode.splitText(startOffset);

					var node = curNode.nextSibling;
					if (!node.nodeValue) {
						console.log(node)
					}
					//if(node.nodeValue){ // 有内容
					selectedNodes.push({
						node: node,
						type: touchNote.SelectedNodeType.text,
						splitType: touchNote.SplitType.head
					});
					//}

				}
				// meet the start-node (begin to traverse)
				withinSelectedRange = true;
			} else if (curNode === endNode) {
				if (curNode.nodeType === 3) {
					var node = curNode;

					node.splitText(endOffset);
					if (!node.nodeValue) {
						console.log(node)
					}
					if (node.nodeValue) { // 有内容
						selectedNodes.push({
							node: node,
							type: touchNote.SelectedNodeType.text,
							splitType: touchNote.SplitType.tail
						});
					}

				}
				// meet the end-node
				break;
			}
			// 处理两个选区中间的文本节点
			else if (withinSelectedRange && curNode.nodeType === 3) {

				if (!curNode.nodeValue) {
					console.log(curNode)
				}
				if (curNode.nodeValue) { // 有内容
					selectedNodes.push({
						node: curNode,
						type: touchNote.SelectedNodeType.text,
						splitType: touchNote.SplitType.none
					});
				}

			}
		}

		return selectedNodes;
	};

	// 获取选中文本
	var getSelectText = function (nodes) {
		var text = "";
		if (nodes) {
			for (var i = 0; i < nodes.length; i++) {
				text += nodes[i].node.textContent;
			}
		}
		return text;
	}
	// 获取选中的html文本(暂未实现)
	var getSelectHtmlText = function (nodes) {
		var text = "";
		return text;
	}

	// 获取前一个文本偏移量 root: Node, text: Node
	function getTextPreOffset(root, text) {
		var nodeStack = [root];
		var curNode = null;
		var offset = 0;
		while (curNode = nodeStack.pop()) {
			var children = curNode.childNodes;
			for (let i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}

			if (curNode.nodeType === 3 && curNode !== text) {
				offset += curNode.textContent.length;
			}
			else if (curNode.nodeType === 3) {
				break;
			}
		}

		return offset;
	}
	// 是否是预期的元素
	var isExcepted = function (e, exceptSelectors) {
		if (exceptSelectors) {
			return $(e).is(exceptSelectors);
		}
		return true;
	}
	/**
	 * 寻找未高亮包裹的原始dom父类
	 */
	// node: HTMLElement | Text: HTMLElement
	var getOriginParent = function (node, parSelectors) {
		if (node instanceof HTMLElement && (!node.dataset || (!node.dataset[touchNote.CAMEL_DATA_ID] && !node.dataset[touchNote.CAMEL_DTMP_DATA_ID])) && isExcepted(node, parSelectors)) {
			return node;
		}

		var originParent = node.parentNode;

		// ?.安全输出,es6写法，不兼容 originParent?.dataset originParent && originParent.dataset
		// 如果存在高亮(或临时选中)属性则继续找父类，即高亮(或临时选中)不作为父类
		while ((originParent && (originParent.dataset[touchNote.CAMEL_DATA_ID] || originParent.dataset[touchNote.CAMEL_DTMP_DATA_ID])) || !isExcepted(originParent, parSelectors)) {
			originParent = originParent.parentNode;
		}
		return originParent;
	};
	// 计算根节点下node所在的tag的下标 node{Node}, root{Document | HTMLElement} :return number 
	var countGlobalNodeIndex = function (node, root) {
		var tagName = node.tagName;
		var list = root.getElementsByTagName(tagName);

		for (let i = 0; i < list.length; i++) {
			if (node === list[i]) {
				return i;
			}
		}

		return touchNote.UNKNOWN_IDX;
	};
	// 获取dom源数据
	// node: HTMLElement | Text, offset: number, root: Document | HTMLElement
	var getDomMeta = function (node, offset, root, parSelectors) {
		var originParent = getOriginParent(node, parSelectors);
		var index = originParent === root ? touchNote.ROOT_IDX : countGlobalNodeIndex(originParent, root);

		var preNodeOffset = getTextPreOffset(originParent, node);
		var tagName = originParent.tagName;

		return {
			parentTagName: tagName,
			parentIndex: index,
			textOffset: preNodeOffset + offset,
		};
	};

	// 通过偏移量获取子类文本节点和相对偏移量 parentNode: Node, offset: number): DomNode
	function getTextChildByOffset(parent, offset) {
		var nodeStack = [parent];
		var curNode = null;
		var curOffset = 0;
		var startOffset = 0;
		while (curNode = nodeStack.pop()) {
			var children = curNode.childNodes;
			for (let i = children.length - 1; i >= 0; i--) {
				nodeStack.push(children[i]);
			}
			if (curNode.nodeType === 3) {
				startOffset = offset - curOffset;
				curOffset += curNode.textContent.length;
				if (curOffset >= offset) {
					break;
				}
			}
		}
		if (!curNode) {
			curNode = parent;
		}
		return { node: curNode, offset: startOffset };
	}

	/**
	 * 通过HighlightSource对象的meta信息获取开始结束父类元素
	 *
	 * @param {HighlightSource} 高亮源对象
	 * @param {HTMLElement | Document} root根节点, 默认document.body
	 * @return {Object} { start, end }
	 */
	// hs: HighlightSource, root: Document | HTMLElement): { start: Node; end: Node }
	var queryElementNode = function (hs, root) {
		var start =
			hs.startMeta.parentIndex === touchNote.ROOT_IDX
				? root
				: root.getElementsByTagName(hs.startMeta.parentTagName)[hs.startMeta.parentIndex];

		var end = hs.endMeta.parentIndex === touchNote.ROOT_IDX
			? root
			: root.getElementsByTagName(hs.endMeta.parentTagName)[hs.endMeta.parentIndex];
		return { start, end };
	};

	// 格式化dom节点对象，n: DomNode): DomNode
	var formatDomNode = function (n) {
		if (
			// Text
			n.node.nodeType === 3 ||
			// CDATASection
			n.node.nodeType === 4 ||
			// Comment
			n.node.nodeType === 8
		) {
			return n;
		}

		return {
			node: n.node.childNodes[n.offset],
			offset: 0,
		};
	};


	// el: HTMLElement, className?: string[] | string): HTMLElement
	var addClass = function (el, className) {
		var classNames = Array.isArray(className) ? className : [className];

		$.each(classNames, function (i, n) {
			$(el).addClass(n);
		});

		return el;
	};
	/**
	 * support IE 10
	 */
	// arr: T[]): T[]
	var unique = function (arr) {
		var res = [];

		for (var i = 0; i < arr.length; i++) {
			var el = arr[i];
			if (res.indexOf(el) === -1) {
				res.push(el);
			}
		}

		return res;
	};

	/**
	 * 部分包裹，拆分节点 Split and wrapper each one.
	 */
	// selected: SelectedNode, range: HighlightRange, className: string[] | string, wrapTag: string,: HTMLElement
	var wrapPartialNode = function (selected, range, className, wrapTag) {
		var wrapNode = document.createElement(wrapTag);

		var parentNode = selected.node.parentNode;
		var prevNode = selected.node.previousSibling;
		var nextNode = selected.node.nextSibling;
		var fragment = document.createDocumentFragment();
		var parentId = parentNode.dataset[touchNote.CAMEL_DATA_ID];
		var parentExtraId = parentNode.dataset[touchNote.CAMEL_DATA_ID_EXTRA];
		var extraInfo = parentExtraId ? parentId + touchNote.ID_DIVISION + parentExtraId : parentId;

		wrapNode.setAttribute('data-' + touchNote.DATA_ID, range.id);
		wrapNode.setAttribute('data-' + touchNote.DATA_ID_EXTRA, extraInfo);
		wrapNode.appendChild(selected.node.cloneNode(false));

		var headSplit = false;
		var tailSplit = false;
		var splitType;

		if (prevNode) {
			var cloneNode = parentNode.cloneNode(false);

			cloneNode.textContent = prevNode.textContent;
			fragment.appendChild(cloneNode);
			headSplit = true;
		}

		var classNameList = [];

		if (Array.isArray(className)) {
			classNameList.push(...className);
		} else {
			classNameList.push(className);
		}

		addClass(wrapNode, unique(classNameList));
		fragment.appendChild(wrapNode);

		if (nextNode) {
			var cloneNode = parentNode.cloneNode(false);

			cloneNode.textContent = nextNode.textContent;
			fragment.appendChild(cloneNode);
			tailSplit = true;
		}

		if (headSplit && tailSplit) {
			splitType = touchNote.SplitType.both;
		} else if (headSplit) {
			splitType = touchNote.SplitType.head;
		} else if (tailSplit) {
			splitType = touchNote.SplitType.tail;
		} else {
			splitType = touchNote.SplitType.none;
		}

		wrapNode.setAttribute('data-' + touchNote.DATA_SPLIT_TYPE, splitType);
		parentNode.parentNode.replaceChild(fragment, parentNode);

		return wrapNode;
	};
	/**
	 * 完全未被包裹，则直接包裹该部分 Wrap a common wrapper.
	 */
	// selected: SelectedNode, range: HighlightRange, className: string[] | string, wrapTag: string): HTMLElement
	var wrapNewNode = function (selected, range, className, wrapTag) {
		var wrapNode = document.createElement(wrapTag);

		addClass(wrapNode, className);

		wrapNode.appendChild(selected.node.cloneNode(false));
		selected.node.parentNode.replaceChild(wrapNode, selected.node);

		wrapNode.setAttribute('data-' + touchNote.DATA_ID, range.id);
		wrapNode.setAttribute('data-' + touchNote.DATA_SPLIT_TYPE, selected.splitType);
		wrapNode.setAttribute('data-' + touchNote.DATA_ID_EXTRA, '');

		return wrapNode;
	};
	// 移除所有的样式
	var removeAllClass = function (el) {
		el.className = '';
	};
	// 是否是空节点
	var isEmptyNode = function (n) { return !n || !n.textContent; };
	// node{HTMLElement}, root{RootElement}:return string
	var getHighlightId = function (node, root) {
		node = findAncestorWrapperInRoot($node, $root);

		if (!node) {
			return '';
		}
		// node{HTMLElement}, root{RootElement}:return HTMLElement
		var findAncestorWrapperInRoot = function (node, root) {
			var isInsideRoot = false;
			var wrapper = null;

			while (node) {
				if (touchNote.isHighlightWrapNode(node)) {
					wrapper = node;
				}

				if (node === root) {
					isInsideRoot = true;
					break;
				}

				node = node.parentNode;
			}

			return isInsideRoot ? wrapper : null;
		};

		return node.dataset[touchNote.CAMEL_DATA_ID];
	};

	/**
	 * 完全被包裹的文本段，只更新idJust update id info (no wrapper updated).
	 */
	// selected{SelectedNode}, range{HighlightRange}, className{string[] | string)} :return HTMLElement
	var wrapOverlapNode = function (selected, range, className) {
		var parentNode = selected.node.parentNode;
		var wrapNode = parentNode;

		removeAllClass(wrapNode);
		addClass(wrapNode, className);

		var dataset = parentNode.dataset;
		var formerId = dataset[touchNote.CAMEL_DATA_ID];

		dataset[touchNote.CAMEL_DATA_ID] = range.id;
		dataset[touchNote.CAMEL_DATA_ID_EXTRA] = dataset[touchNote.CAMEL_DATA_ID_EXTRA]
			? formerId + touchNote.ID_DIVISION + dataset[touchNote.CAMEL_DATA_ID_EXTRA]
			: formerId;

		return wrapNode;
	};
	// 
	var walkToRenderCommentBlock = function (range, lastRightOffset, currentAnnotationId, lastAnnotationByIdNode) {
		var currentRightOffset = range.domRect.right; //距离右侧位置

		if (lastRightOffset > currentRightOffset) {
			// 找到了插入点
			doRenderCommentBlock(range, currentAnnotationId, lastAnnotationByIdNode)
		} else {
			// 继续向下一个字符寻找
			if (range.endOffset < range.endContainer.textContent.length - 1) {
				// 如果当前 range 还没找到头，那就继续下一个
				var currentRange = document.createRange();
				currentRange.setStart(range.endContainer, range.endOffset);

				// 如果结束节点类型是 Text, Comment, or CDATASection 之一, 那么 endOffset 指的是从结束节点算起字符的偏移量
				// 对于其他 Node 类型节点， endOffset 是指从结束结点开始算起子节点的偏移量。

				try {
					// 存在 range.endOffset + 1 不存在的情况 (比如空标签)，这时候就用下一个节点
					currentRange.setEnd(range.endContainer, range.endOffset + 1);
				} catch (e) {
					currentRange.setStart(getNextSiblingNode(range.endContainer), 0);
					currentRange.setEnd(getNextSiblingNode(range.endContainer), 1);
				}

				return walkToRenderCommentBlock(currentRange, currentRightOffset, currentAnnotationId, lastAnnotationByIdNode);
			} else {
				// 如果当前 range 到头了，还没有找到，则找下一个 nodeText
				var nextNode = getNextTextNode(range.endContainer);
				if (nextNode) {
					var currentRange = document.createRange();
					currentRange.setStart(nextNode, 0);
					currentRange.setEnd(nextNode, 1);
					return walkToRenderCommentBlock(currentRange, currentRightOffset, currentAnnotationId, lastAnnotationByIdNode);
				} else {
					// 找到了当前段的最后面，在段后加
					doRenderCommentBlock(null, currentAnnotationId, lastAnnotationByIdNode);
				}
			}
		}
	}

	var highlightNoteAfter = function (hlele, noteEle, data, tipsClass, tipsContentClass, containerE, options) {

		// 点击弹出修改窗口
		var url = containerE.data('url-upd');
		hlele.off('click.touchnote').on('click.touchnote', function (event) {
			var datas = hlele.data();
			var params = {
				...data,
				keycode: JSON.stringify(datas.keycode),
				keyid: datas.keycode.id,
				startTag: datas.keycode.startMeta.parentTagName,
				startTagIndex: datas.keycode.startMeta.parentIndex,
				startTextOffset: datas.keycode.startMeta.textOffset,
				endTag: datas.keycode.endMeta.parentTagName,
				endTagIndex: datas.keycode.endMeta.parentIndex,
				endTextOffset: datas.keycode.endMeta.textOffset,
				text: datas.keycode.text,
				htmltext: datas.keycode.text,
				remarks: data.remarks
			};
			// 打开弹出框			
			options.openDialog(hlele, touchNote.OPERATE_Type.UPDATE, url, params, '修改批注', containerE, options);
			return false; //防止冒泡
		});
	}
	function warapTmpHighlight(nodes, hltmpClsName, id) {
		$.each(nodes, function (i, n) {
			var tmpEle = $(n.node);

			tmpEle.wrap('<span class="' + hltmpClsName + '" data-' + touchNote.TMP_DATA_ID + '="' + id + '"></span>');
		});
	}
	function removeTmpHighlight(hltmpClsName) {
		$('.' + hltmpClsName).each(function (i, n) {

			$(n.childNodes[0]).unwrap(); //取第一个子类剥离父类，下面的方法不是用使因为没有更新dom树，parentNode或parent()依然能查到
			// 等同于 $(n).replaceWith( n.childNodes );

			//var prev = n.previousSibling;
			//var next = n.nextSibling;

			// 方法1：jquery替换 方法2：先添加后移除
			//var tmpEle = $(n);
			//var my_html = tmpEle.html();
			//tmpEle.replaceWith(my_html);
			//tmpEle.before(my_html);
			//tmpEle.remove();			

			// 方法3、使用原始js
			//var parent = n.parentNode;
			//var fr = document.createDocumentFragment();

			//$.each(n.childNodes, function(i, c){ //循环子节点

			//fr.appendChild(c.cloneNode(false)); //浅克隆=标签里的元素，属性、事件没有复制。
			//});
			//parent.replaceChild(fr, n); // 将高亮用新生成的元素替换

			// 合并相邻的文本节点
			//normalizeSiblingText(prev, true);
			//normalizeSiblingText(next, false);			
		});
	}
	// 释放鼠标处理函数
	function mouseUp(event, root, ele, options) {

		// 移除临时高亮
		removeTmpHighlight(options.hltmpClsName);

		var text = touchNote.getSelectionText();
		var noteToolEle = $(".xml-note-tool-box");
		if (text && touchNote.IS_MARK) {//	选中文本切可以标注
			var selection = touchNote.getSelection(root); //获取selection eles.getSelection();	

			htmltext = selection.text; //html
			var serializeStr = touchNote.serializeSelection(root, selection, options.parSelectors);
			var range = selection.range;

			var target = event.target;
			var targetE = $(target);
			if (targetE.length) {
				// 执行高亮
				if (noteToolEle.length == 0) {
					var html = options.tooltopsHtml;
					noteToolEle = $('<div class="xml-note-tool-box" style="display:none;">' + html + '</div>');
					$("body").append(noteToolEle);
					noteToolEle.find('.note-tool-item').each(function (i, n) {
						var tooEle = $(n);

						tooEle.off('click.touchnote').on('click.touchnote', function (event) {
							event.stopPropagation();
							var selection = ele.data('selection'); //data中取
							// 内置了划线、批注、复制3种方法，需要样式对应，其他的可以通过重写tooltopsHtml和tooltopsClickOthers方法实现
							if (tooEle.hasClass('icon-bookmark') || tooEle.find('.icon-bookmark').length) { // 书签								
								options.tooltopsClickBookmark(tooEle, root, ele, selection, options);
							}else if (tooEle.hasClass('icon-line') || tooEle.find('.icon-line').length) { // 划线								
								options.tooltopsClickLine(tooEle, root, ele, selection, options);
							} else if (tooEle.hasClass('icon-note') || tooEle.find('.icon-note').length) { //批注															
								options.tooltopsClickNote(tooEle, root, ele, selection, options);
							} else if (tooEle.hasClass('icon-copy') || tooEle.find('.icon-copy').length) { //复制
								options.tooltopsClickCopy(tooEle, root, ele, selection, options);
							} else {
								options.tooltopsClickOthers(tooEle, root, ele, selection, options);
							}

							noteToolEle.hide(); //点击完成后隐藏							
						});
					});
				}
				ele.data('selection', selection); //放置到data中后面再取
				noteToolEle.removeClass('up').addClass('down'); //移除，重新定位
				noteToolEle.show();

				var domRect = range.domRect;

				var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; //左右滚动条偏移量
				var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量
				var x = domRect.x + scrollX + domRect.width / 2; // 距窗口左侧距离 + 滚动条偏移 + 选区宽度的一半 - 提示栏的宽度一半
				var y = domRect.y + scrollY + domRect.height + 10;

				ele.data('dialog-x', x);

				x -= noteToolEle.width() / 2; // 距窗口左侧距离 + 滚动条偏移 + 选区宽度的一半 - 提示栏的宽度一半

				var height = $(window).height() + scrollY;
				if (y + noteToolEle.height() > height) { //下方显示不开，显示到上方
					y = domRect.y + scrollY - noteToolEle.height() - 10;
					noteToolEle.removeClass('down').addClass('up');
				}
				ele.data('dialog-y', y);

				noteToolEle.offset({ left: x, top: y });
			}

			// 获取选中的节点
			var nodes = selection.selectedNodes;

			// 临时高亮处理		
			warapTmpHighlight(nodes, options.hltmpClsName, "tmp-tn-" + new Date().getTime());
			return false;
		} else if (noteToolEle.length) { // 未选中则隐藏提示栏				
			noteToolEle.hide();
		}
	}
	// 鼠标划过的处理函数
	function mouseOver(event, root, ele, options) {
		var target = event.target;
		if (options.mouseOverBefore(event, root, ele, options) && touchNote.isHighlightWrapNode(target)) {
			//在高亮节点
			var targetEle = $(target);
			options.mouseOverInHigh(targetEle, event, root, ele, options);

		}
		options.mouseOverAfter(event, root, ele, options);

	}
	// 鼠标划过的前置处理，如果返回false则不继续执行了
	function mouseOverBefore(event, root, ele, options) {
		//var noteClassName = options.hlClsName; // 批注样式
		//$("."+noteClassName).removeClass("selected");
		return true;
	}
	// mouseover事件结束方法
	function mouseOverAfter(event, root, ele, options) {
		// 待处理
	}
	// 鼠标划过时再高亮区的处理
	function mouseOverInHigh(targetEle, event, root, ele, options) {
		// 待处理
	}
	// 窗口的竖向是否有滚动条
	function hasScrollbar() {
		return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
	}
	// 向ele中添加属性
	function addDatas(ele, data) {
		ele.attr('data-nid', data.nid);
		ele.attr('data-remarks', data.remarks); //批注内容
		ele.attr('data-keycode', JSON.stringify(data.keycode)); //对象转字符串，用data取时是对象				
		ele.attr('data-keyid', data.keyid);

		ele.attr('data-userid', data.userid);
		ele.attr('data-username', data.username);
		ele.attr('data-createtime', data.createtime);
	}
	// 监听滚动事件
	var lastScrolly = 0;

	setTimeout(function () {
		// 触发滚动，以便刷新时显示在正常位置
		$(window).triggerHandler('scroll.touchnote');
	}, 500); //延时	

	// 扩展对外属性
	$.extend($.fn.touchNote, {
		// 触发的方法
		triggerMethod: ["blur"],//,"keyup"
		// 默认参数
		defaults: {
			/** 根对象 */
			root: document.body, // document.body;
			/** 用户名，静态演示用，动态数据一般不需要从session或cookie中获取 */
			username: '兔先生',
			/** 用户头像，静态演示用，动态数据一般不需要从session或cookie中获取 */
			userimage: '',
			/** 父节点选择器，只有符合选择器的才作为父节点，用于计算文本的相对偏移量，尽量使用固定不变的节点选择器减少内容变动引起的节点变化，如段落或内容的顶级容器 */
			parSelectors: '',
			/** 批注容器jquery选择器 */
			noteContainerSelector: '.notesshow_container_div',
			/** 批注数量选择器 */
			noteCountSelector: '.notesshow_container_div .notesCount',
			/** 批注条目样式名称 */
			noteItemClsName: 'notesdiv',
			/** 批注条目选中样式名称 */
			noteSelectedClsName: 'noteselected',
			/** 高亮包裹标签 */
			hltagName: 'span',
			/** 临时高亮样式名 */
			hltmpClsName: "tmp_touch_notes",
			/** 高亮样式名 */
			hlClsName: "touch_notes",
			/** 高亮初始化样式名 */
			hlIntClsName: "nodetips-init",
			/** 高亮数据，数据字段{'id':id, 'keycode':keycode, 'remarks':content, 'username':username, 'createtime':createtime} */
			hldatas: [],
			/** 元素高亮后的处理方法，参数(ele, data, tipsClass, tipsContentClass, options) */
			highlightNoteAfter: highlightNoteAfter,
			/** mouseup事件方法 */
			mouseup: mouseUp,
			/** mouseover事件方法 */
			mouseover: mouseOver,
			/** mouseover事件开始处理事件，返回boolean,为false则不往下进行,参数(event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverBefore: mouseOverBefore,
			/** mouseover事件在高亮节点的处理事件,(targetEle, event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverInHigh: mouseOverInHigh,
			/** mouseover事件结束方法,参数(event, root, ele, options)，如果重定义了mouseover则不调用 */
			mouseOverAfter: mouseOverAfter,
			/** 高亮节点的鼠标划过事件，数组，第一个function为鼠标移入事件，第二个为移出事件，分别为增加/移出selected样式 */
			highlightNoteMouseover: [function (event, root, ele, options) {
				var noteClassName = options.hlClsName; // 批注样式
				$("." + noteClassName).removeClass("selected");

				var highlightId = ele.data(touchNote.DATA_ID);

				var codeNodeEles = $('.' + noteClassName + "[data-" + touchNote.DATA_ID + "='" + highlightId + "']");
				codeNodeEles.addClass('selected'); //当前选中高亮
				// 选区重叠时，重叠的也显示
				// 扩展，这个判断存在问题，如果DATA_ID里面的值比较短，可能存在不相等也包含的情况，如11和1,12和1等				
				var codeNodeExtraEles = $('.' + noteClassName + "[data-" + touchNote.DATA_ID_EXTRA + "*='" + highlightId + "']");
				codeNodeExtraEles.addClass('selected');

			}, function (event, root, ele, options) {
				var noteClassName = options.hlClsName; // 批注样式
				$("." + noteClassName).removeClass("selected");
			}],
			/** 弹出框html，提交按钮为submitbtn，删除按钮为deletebtn */
			dialogHtml: '<div class="edit-textarea-wrap"><textarea class="edit-textarea"></textarea><button class="submitbtn">提交</button><button class="deletebtn">删除</button></div>',
			/** 打开弹出框， */
			openDialog: function (invokerEle, keytype, url, params, title, containerE, options) {

				var dialogEle = $(".note-dialog");
				// 创建弹出框
				if (dialogEle.length == 0) {
					var html = options.dialogHtml;
					dialogEle = $('<div class="note-dialog down" style="top:20px;">' + html + '</div>');
					$("body").append(dialogEle);
				}

				var x = containerE.data('dialog-x'); //选中区域时记录			
				var y = containerE.data('dialog-y');

				dialogEle.find("textarea.edit-textarea").val(''); //清空输入框

				// 控制按钮显示
				if (keytype == touchNote.OPERATE_Type.UPDATE) { //修改		
					dialogEle.find(".deletebtn").show(); // 显示删除按钮
					// var e = event || window.event;
					x = invokerEle.offset().left + invokerEle.outerWidth(true) / 2;
					y = invokerEle.offset().top + invokerEle.outerHeight(true) + 8;

					dialogEle.find("textarea.edit-textarea").val(invokerEle.data('remarks'));

				} else if (keytype == touchNote.OPERATE_Type.ADD) { //添加
					dialogEle.find(".deletebtn").hide(); // 隐藏删除按钮
				}
				dialogEle.data('keytype', keytype);
				dialogEle.data('invoker', invokerEle);

				var initdata = dialogEle.data('inidate'); //初始化数据，一次即可
				if (!initdata) {
					dialogEle.data('inidate', true);
					dialogEle.off('click.touchnote').on('click.touchnote', function () {

						return false; //防止冒泡，点击弹出框内内容时被关闭
					});
					// 保存按钮
					dialogEle.find('.submitbtn').off('click.touchnote').on('click.touchnote', function () {
						var remarks = dialogEle.find("textarea.edit-textarea").val();
						var keytypev = dialogEle.data('keytype');
						if (keytypev == touchNote.OPERATE_Type.UPDATE) { //修改
							// 修改当前元素的值
							var invokerEle = dialogEle.data('invoker');
							// 更新批注
							invokerEle.attr('data-remarks', remarks);
							invokerEle.data('remarks', remarks);
							// 将批注区内容修改了，生产环境中是与后台交互
							var keyid = params.keyid; // 获取keyid						
							// 获取批注区并更新批注区内容
							if (keyid) {
								var noteItemClsName = containerE.getOptions('noteItemClsName'); // 获取对应的批注区
								$("." + noteItemClsName + "[data-keyid='" + keyid + "']").find('.remarks').text(remarks); //点击
							}

						} else if (keytypev == touchNote.OPERATE_Type.ADD) { //添加
							var nodes = containerE.addHighlighter(remarks);
							console.log(nodes);
						}

						// 提交数据，需要自己实现,返回值为true关闭
						if (options.submitData(remarks, keytypev, params, invokerEle, dialogEle, options)) {
							dialogEle.find("textarea.edit-textarea").val('');
							dialogEle.hide(); //隐藏弹出框
						}
					});


					// 删除按钮
					dialogEle.find('.deletebtn').off('click.touchnote').on('click.touchnote', function () {
						// 删除高亮
						var invokerEle = dialogEle.data('invoker');
						touchNote.removeHighlight(containerE, invokerEle.data(touchNote.CAMEL_DATA_ID));
						dialogEle.find("textarea.edit-textarea").val('');
						dialogEle.hide(); //隐藏弹出框					

					});
				}


				// 如果弹出框比窗口还大，那就缩放到窗口大小
				var winWidth = $(window).width();
				if (dialogEle.width() > winWidth) {
					dialogEle.width(winWidth - 8);
				}
				var diaWidth = dialogEle.outerWidth(true);
				var leftv = x - diaWidth / 2;
				// 左边距离不能小于0
				if (leftv < 4) {
					leftv = 5;
				}
				var leftv = x - diaWidth / 2;
				// 右边距离不能小于0
				if (leftv + diaWidth > winWidth) {
					leftv = winWidth - diaWidth - 2;
				}
				dialogEle.show().offset({ left: leftv, top: y });

			},
			/** 数据提交，提交成功返回true，失败返回false，remarks为批注， */
			submitData: function (remarks, keytype, params, ele, dialogEle, options) {
				console.log("数据提交后执行，如保存数据库等, 正式环境下建议覆盖", remarks, keytype, params);
				return true;
			},
			addBookmark: function (invokerEle, keytype, url, params, title, containerE, options) {},
			/** 提示栏html，里面的条目必须用含有note-tool-item的样式，如果使用内置的划线、批注、复制方法则样式必须对应icon-line、icon-note、icon-copy并且包裹在note-tool-item中 */
			// tooltopsHtml: '<div class="note-tool-item"><i class="icon-line">划线</i></div><div class="note-tool-item"><i class="icon-note">批注</i></div><div class="note-tool-item"><i class="icon-copy">复制</i></div>',
			// tooltopsHtml: '<div class="note-tool-item"><i class="icon-bookmark">书签</i></div><div class="note-tool-item"><i class="icon-note">批注</i></div><div class="note-tool-item"><i class="icon-copy">复制</i></div>',
			tooltopsHtml: '<div class="note-tool-item"><i class="icon-note">批注</i></div><div class="note-tool-item"><i class="icon-copy">复制</i></div>',
			/** 提示栏，书签的处理方法 */
			tooltopsClickBookmark: function (tooEle, root, ele, selection, options) {
				var urlAdd = ele.data("url-add"); // 添加的url								

				console.log(selection)
				var text = selection.text;
				var htmltext = selection.text;
				//var serializeStr = touchNote.serializeSelection(root, selection, options.parSelectors);
				var hs = touchNote.getHighlightSource(root, selection, options.parSelectors);
				var serializeStr = touchNote.serialize2Str(hs);

				var params = {
					keycode: serializeStr,
					cvid: ele.data('cvid'),
					keyid: hs.id,
					startTag: hs.startMeta.parentTagName,
					startTagIndex: hs.startMeta.parentIndex,
					startTextOffset: hs.startMeta.textOffset,
					endTag: hs.endMeta.parentTagName,
					endTagIndex: hs.endMeta.parentIndex,
					endTextOffset: hs.endMeta.textOffset,
					text: text,
					htmltext: htmltext
				};
				console.log("🚀 ~ params:", params)
				options.addBookmark(tooEle, touchNote.OPERATE_Type.ADD, urlAdd, params, '添加书签', ele, options);
			},
			/** 提示栏，划线的处理方法 */
			tooltopsClickLine: function (tooEle, root, ele, selection, options) {
				console.log("划线，暂未实现，可自行扩展");
			},
			/** 提示栏，批注的处理方法，ele为容器jquery对象 */
			tooltopsClickNote: function (tooEle, root, ele, selection, options) {
				var urlAdd = ele.data("url-add"); // 添加的url								

				console.log(selection)
				var text = selection.text;
				var htmltext = selection.text;
				//var serializeStr = touchNote.serializeSelection(root, selection, options.parSelectors);
				var hs = touchNote.getHighlightSource(root, selection, options.parSelectors);
				var serializeStr = touchNote.serialize2Str(hs);

				var params = {
					keycode: serializeStr,
					cvid: ele.data('cvid'),
					keyid: hs.id,
					startTag: hs.startMeta.parentTagName,
					startTagIndex: hs.startMeta.parentIndex,
					startTextOffset: hs.startMeta.textOffset,
					endTag: hs.endMeta.parentTagName,
					endTagIndex: hs.endMeta.parentIndex,
					endTextOffset: hs.endMeta.textOffset,
					text: text,
					htmltext: htmltext
				};

				options.openDialog(tooEle, touchNote.OPERATE_Type.ADD, urlAdd, params, '添加批注', ele, options);
			},
			/** 提示栏，复制的处理方法 */
			tooltopsClickCopy: function (tooEle, root, ele, selection, options) {
				let oInput = document.createElement('input')
				oInput.value = selection.text
				document.body.appendChild(oInput)
				oInput.select()
				document.execCommand('Copy')
				oInput.remove()
				removeTmpHighlight(options.hltmpClsName);
			},
			/** 提示栏，除了划线，批注，复制外，其他的事件处理方法，结合tooltopsHtml自定义实现 */
			tooltopsClickOthers: function (tooEle, root, ele, selection, options) {
				console.log("提示栏的其他方法，未实现……");
			},
			/** 添加批注区html内容，有需要时可以覆盖,索引的class为number，用户名class为username，文本class为text，批注class为remarks，创建时间class为createtime。  参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，ele为处理的对象,options为配置*/
			addNoteHtml: function (data, index, datas, noteContainerEle, root, ele, options) {

				var html = '<div class="' + options.noteItemClsName + ' panel panel-default">' +
					'	<div class="panel-heading">' +
					'		<span class="catalog_number_wraper"><span class="catalog_number fs12 number">' + (index + 1) + '</span></span>' +
					'		<h6 class="panel-title fs12 username" style="display: inline-block;">' + data.username + '</h6>' +
					'		<label class="fr menus">…</label>' +
					'	</div>' +
					'	<div class="bs-callout bs-callout-danger text">' + data.keycode.text + '</div>' +
					'	<div class="panel-body fs12 pt5"><div class="fs11 createtime">' + data.createtime + '</div><div class="remarks">' + data.remarks + '		</div></div>' +
					'</div>';
				return html;

			},
			/** 添加批注区html内容，有需要时可以覆盖。  参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，ele为处理的对象,options为配置*/
			getAddNoteEle: function (data, index, datas, noteContainerEle, root, ele, options) {

				// 生成html
				var html = options.addNoteHtml(data, index, datas, noteContainerEle, root, ele, options);
				var noteEle = $(html);
				// 可能存在特殊字符转义,重写赋值
				noteEle.find('.text').text(data.keycode.text);
				noteEle.find('.remarks').text(data.remarks);
				return noteEle;
			},
			fixNoteTopFn: function (noteContainerEle, root, ele, options) {
				var notesContainerDiv = $(options.noteContainerSelector);
				var offset = notesContainerDiv.data('ori_offset');
				if (!offset) {
					offset = notesContainerDiv.offset();
					notesContainerDiv.data('ori_offset', offset);
				}
				var offsetOldVal = 0;
				if (offset) {
					offsetOldVal = offset.top;
				}
				return offsetOldVal;
			},
			/** 右侧批注栏操作html, touch-note-popmenus必须有，里面的条目必须用含有note-tool-item的样式，，修改添加样式touchnote-update，删除添加样式touchnote-delete */
			noteOperateHtml: '<div class="touch-note-popmenus down"><div class="note-tool-item"><i class="touchnote-update">修改</i></div><div class="note-tool-item"><i class="touchnote-delete">删除</i></div></div>',
			/** 添加批注区元素事件 参数(noteEle, noteContainerEle, root, options, datas)，noteEle为批注区jquery元素，noteContainerEle为容器jquery对象，root为根元素，options为配置*/
			addNoteEleFn: function (noteEle, noteContainerEle, root, ele, options) {
				var url = ele.data("url-upd"); // 添加的url							
				noteEle.on("update", function () { //修改

					var datas = noteEle.data();
					var params = {
						keycode: JSON.stringify(datas.keycode),
						keyid: datas.keycode.id,
						startTag: datas.keycode.startMeta.parentTagName,
						startTagIndex: datas.keycode.startMeta.parentIndex,
						startTextOffset: datas.keycode.startMeta.textOffset,
						endTag: datas.keycode.endMeta.parentTagName,
						endTagIndex: datas.keycode.endMeta.parentIndex,
						endTextOffset: datas.keycode.endMeta.textOffset,
						text: datas.keycode.text,
						htmltext: datas.keycode.text,
						remarks: datas.remarks
					};
					// 打开弹出框
					options.openDialog(noteEle, touchNote.OPERATE_Type.UPDATE, url, params, '修改批注', ele, options);

				}).on("delete", function () { //删除

					// 可能需要需要后台交互，请重写
					var params = { id: noteEle.data("nid") };

					//var nodes = noteEle.triggerHandler("highlightele"); //获取对应的高亮元素
					//nodes.removeClass(codeNoteClassName+" touchjs-tips "+hlIntClsName).removeData("target").removeAttr("data-target").off(".touchjs_tips");

					touchNote.removeHighlight(ele, noteEle.data("keycode").id);

					noteEle.remove();
					// 更新批注总数 
					noteContainerEle.triggerHandler("refreshCount");

				}).on("showInVisible", function () { // 显示到可视区
					var $this = noteEle;
					var notesContainerDiv = $(options.noteContainerSelector);
					var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
					var height = winHeight + scrollY;
					var extendHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
					//if(!scrollY){ //如果没有滚动条，则到原来的高度就可以了，不需要到最顶部,顶部存在固定元素所以距顶部高度时固定的
					//extendHeight = offsetOldVal;
					//}
					if ($this.height() < winHeight) { //未超过1屏，则全部显示到可视区			
						var eleHeight = $this.offset().top + $this.height();
						if (eleHeight > height || $this.offset().top < scrollY) { //可视区下方 || 可视区上方
							notesContainerDiv.offset({ top: scrollY - ($this.offset().top - notesContainerDiv.offset().top) + extendHeight })
						}
					} else { // 超过1屏
						if ($this.offset().top > height || $this.offset().top < scrollY) { //可视区下方 || 可视区上方							
							notesContainerDiv.offset({ top: scrollY - ($this.offset().top - notesContainerDiv.offset().top) + extendHeight })
						}
					}
				}).on("highlightele", function () { //获取对应的高亮元素
					var $this = noteEle;
					var dataid = $this.data('keyid');
					var keycode = $this.data("keycode");
					var keyid = keycode.id;

					var hlClsName = ele.getOptions('hlClsName'); //高亮样式

					// 不通过nid，通过data-highlight-id查找					
					var nodes = ele.find("." + hlClsName + "[data-" + touchNote.DATA_ID + "='" + dataid + "']");

					// 合并重叠部分
					nodes = nodes.add(ele.find("." + hlClsName + "[data-highlight-id-extra*='" + keyid + "']"));
					return nodes;
				}).on("click", function () { //点击则内容区高亮，并且拉到可视范围内

					var fixedHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
					var hlClsName = ele.getOptions('hlClsName'); //高亮样式
					var $this = noteEle;
					// 选中样式处理
					$('.' + options.noteItemClsName).removeClass(options.noteSelectedClsName);
					$this.addClass(options.noteSelectedClsName);

					// 获取高亮节点
					var nodes = $this.triggerHandler("highlightele");

					// 先移除高亮，再添加高亮
					ele.find("." + hlClsName).removeClass('selected');
					nodes.addClass('selected');

					if (nodes.length) {
						var nodeFirst = nodes.first(); //取第一个元素
						// 如果不再可视区内，则滚动到可视区
						var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
						var height = winHeight + scrollY;

						// offset是不会随着滚动条的变化而变化的
						if (hasScrollbar() && (nodeFirst.offset().top > height || nodeFirst.offset().top < scrollY + fixedHeight)) { //y轴出现滚动条且超出可视范围则滚动
							//滚动到距窗口顶部50px的位置
							$("html,body").animate({ scrollTop: (nodeFirst.offset().top - 80) + "px" }, 800, function () {
								// 该元素也需要在可视区,如果不在，拉倒可视区内							
								$this.triggerHandler("showInVisible");
							});

						} else {
							$this.triggerHandler("showInVisible");
						}

					}

				});

				// 操作方法
				noteEle.find(".menus").on('click', function () {
					var menusE = $(this);

					var popEles = $('.touch-note-popmenus');
					if (!popEles.length) {
						var popHtml = options.noteOperateHtml;
						popEles = $(popHtml);
						$("body").append(popEles);
					}
					// 显示，并调整显示位置
					popEles.show().offset({ left: menusE.offset().left + menusE.width() - popEles.width() + 13, top: menusE.offset().top + menusE.outerHeight(true) });

					popEles.find('.note-tool-item').each(function (i, n) {
						var itemEle = $(n);

						itemEle.off('click.touchnote').on('click.touchnote', function () {
							// 内置了修改、删除2种方法，需要样式对应，其他的可以通过重写tooltopsHtml和tooltopsClickOthers方法实现
							if (itemEle.hasClass('touchnote-update') || itemEle.find('.touchnote-update').length) { // 修改						
								options.noteUpdateFn(noteEle, itemEle, root, options);
							} else if (itemEle.hasClass('touchnote-delete') || itemEle.find('.touchnote-delete').length) { // 删除								
								options.noteDeleteFn(noteEle, itemEle, root, options);
							} else {
								options.noteClickOtherFn(noteEle, itemEle, root, options);
							}
						});
					});

					return false;
				});

				var initdocclick = noteContainerEle.data("initdocclick");
				if (!initdocclick) {
					noteContainerEle.data("initdocclick", true);
					// 取消无效的高亮显示
					$(document).off("click.note.touchnote").on("click.note.touchnote", function () {
						$('.touch-note-popmenus').hide();
					});
				}

			},
			/** 批注更新 参数(noteEle, itemEle, root, options)，noteEle为批注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteUpdateFn: function (noteEle, itemEle, root, ele, options) {
				noteEle.triggerHandler("update");
			},
			/** 批注删除 参数(noteEle, itemEle, root, options)，noteEle为批注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteDeleteFn: function (noteEle, itemEle, root, ele, options) {
				noteEle.triggerHandler("delete");
			},
			/** 批注其他方法 参数(noteEle, itemEle, root, options)，noteEle为批注对象，itemEle为操作条目对象，root为根元素，options为配置*/
			noteClickOtherFn: function (noteEle, itemEle, root, ele, options) {
				console.log("划线，暂未实现，可自行扩展");
			},
			/** 添加批注区内容 参数(data, index, noteContainerEle, root, options, datas)，data为数据，index为下标,datas为所有数据，noteContainerEle为容器jquery对象，root为根元素，options为配置*/
			addNoteEle: function (data, index, datas, noteContainerEle, root, ele, options) {
				// 生成html对应的jquery对象
				var noteEle = null;
				if (noteContainerEle.length) {
					noteEle = options.getAddNoteEle(data, index, datas, noteContainerEle, root, ele, options);

					// 添加属性
					addDatas(noteEle, data);
					// 添加元素事件
					options.addNoteEleFn(noteEle, noteContainerEle, root, ele, options);

					noteContainerEle.append(noteEle);

					var scrollbind = noteContainerEle.data('scrollbind');
					if (!scrollbind) {
						noteContainerEle.data('scrollbind', true);

						var offset = noteContainerEle.offset();
						noteContainerEle.data('ori_offset', offset);
						noteContainerEle.data('ori_position', noteContainerEle.css('position'));
						noteContainerEle.data('ori_width', noteContainerEle.width());

						var fixedHeight = options.fixNoteTopFn(noteContainerEle, root, ele, options);
						// 暂时不支持多个
						$(window).off("scroll.note.touchnote").on("scroll.note.touchnote", function (e) {
							var scrollY = document.documentElement.scrollTop || document.body.scrollTop; //上下滚动条偏移量,即上部遮挡内容的高度
							// 需要保证批注内容一直在可视区域内，如果超过一屏则居底显示最后几个，如果不超过1屏则居上显示全部，如果没有滚动条了则回复原始值
							if (scrollY) { //出现滚动条，计算顶部距离
								console.log(noteContainerEle.height() + " " + winHeight)
								if (noteContainerEle.height() > winHeight) { //大于1屏，超出窗口高度才处理，否则不处理
									if (noteContainerEle.height() + noteContainerEle.offset().top < scrollY + winHeight) { // 底部要留白了
										noteContainerEle.offset({ top: scrollY - (noteContainerEle.height() - winHeight) - 20 });
									}
									if (lastScrolly > scrollY && noteContainerEle.offset().top > scrollY) { //向上滚动

										noteContainerEle.offset({ top: scrollY + fixedHeight });
									}
								} else { //小于1屏，50是因为又顶部固定的元素
									noteContainerEle.css({ 'position': 'fixed', 'top': fixedHeight, 'left': noteContainerEle.data('ori_offset').left }).width(noteContainerEle.data('ori_width'));
								}
							} else {
								// 还原
								noteContainerEle.css('position', noteContainerEle.data('ori_position'));
								noteContainerEle.offset(noteContainerEle.data('ori_offset'));
							}
							lastScrolly = scrollY;
							// 隐藏右侧操作栏
							$('.touch-note-popmenus').hide();
						});
					}
				}

				return noteEle;

			},
			/** 更新批注总数 参数(count, datas, root, options)，count为总数，datas为高亮数据，root为根元素，options为配置 */
			updateNotesCount: function (count, datas, root, ele, options) {
				$(options.noteCountSelector).text(count);
			},
			/** 刷新批注区总数 参数(count, datas, root, options)，count为总数，datas为高亮数据，root为根元素，options为配置 */
			refreshNotesCount: function (noteContainerEle, root, ele, options) {
				// 更新批注总数 count, datas, root, options
				options.updateNotesCount(noteContainerEle.find("." + ele.getOptions('noteItemClsName')).length, null, root, ele, options);
				// 更新数目
				noteContainerEle.find("." + ele.getOptions('noteItemClsName')).each(function (i, n) {
					console.log(i);
					console.log(n);
					$(n).find(".number").text(i + 1);
				});
			}
		}
	});

})(jQuery);
