自动生成文章目录导航及目录相关UI -- 算法解析-- 原生jq实现

文章目录有关

先上git地址,README完整,有需自取https://github.com/xxx407410849/AutoCatelog 求星哦

自动生成文章目录的基本原理是对所选容器里的所有子节点进行遍历,得到H1 - H6的标签序列,再在顺序结构的基础上创建(模拟)出一棵树,因为默认大标题包含小标题的条目(如H1 H2,则H1包含H2的条目),所以在这棵树上必不存在HX->HN(X>=N)的树枝(即出现H3 H2的情况下,程序不可能分析H2成为H3子条目),抽取出这棵树后,再进行有关的目录UI操作。

当然,若是单纯的想要生成目录,则无需模拟树,只需要根据H标签的顺序和H标签的层级给予<li>标签相应的margin-left值,就能模拟出目录结构了,但是那样则不可能实现某些帅气的UI。


UI有关

一般与目录相关的有以下UI

点击目录自动滚动到相应的标题(锚点)

目录的路径标红

目录的滚动展开

目录条目的高亮选取与滚动检测

作者演示如下:

没错这是作者自己的博客,还没完工的,富文本编辑器用的自己的,有兴趣的可以看一下我的富文本编辑器项目https://github.com/xxx407410849/Leditor


抽取标签的顺序序列

//目录检测
var $articleElem = $('.article-body-text');
var $articleChildElem = $articleElem.children();
var hReg = /^h/i;
var $hCtn = [];
var $liCtn = [];
var $wayList = [];
var $setList = [];
//鉴定存在的最高级H,最低级H
//实际上推荐只使用H3 H4 H5
var $minH = "H6";
var $maxH = "H1";
for(var i = 0 ; i < $articleChildElem.length ; i++){
	if(hReg.test($articleChildElem[i].nodeName)){
		$hCtn.push($articleChildElem[i]);
		if($minH > $articleChildElem[i].nodeName){
			$minH = $articleChildElem[i].nodeName;
		}
		if($maxH < $articleChildElem[i].nodeName){
			$maxH = $articleChildElem[i].nodeName;
		}
	}
}

代码中 $articleElem 是所选取的要生成目录的文章容器,鉴定所使用最高和最低的H的原因是因为需要适配存在不同数量H标签的情况,给予不同的样式。

同时将选取到的H标签按照顺序push至数组$hCtn中。

目录排序与UI锚点定位

//目录排序,提取
//实际上用margin模拟排序,而不是真的有顺序
var minHNum = parseInt($minH.slice(1,2));
var maxHNum = parseInt($maxH.slice(1,2));
var $ulElem = $("<ul class='catalog'></ul>");
//点击事件绑定以及抽取标题文字
$hCtn.forEach(function(item){
	var $liElem = $("<li class='catalogitem'></li>")
	$liElem.addClass(item.nodeName + "item");
	$liElem.text(item.innerHTML);
	$liElem.on('click',function(e){
		var $elem = e.target;
		var $idx = $('.catalogitem').index(e.target);
		console.log($idx);
		var $elemTop = $hCtn[$idx].offsetTop;
		$('html,body').animate({
			scrollTop : $elemTop + "px"
		},500);
	});
	if(item.nodeName != $minH)$liElem.addClass('unset');
	//存储liElem
	$liCtn.push($liElem);
	$ulElem.append($liElem);
});
//插入至容器
$('.catelog-ctn').append($ulElem);
for(var i = maxHNum+1 ; --i ; i > minHNum){
	var ClassStr = ".H" + i + "item";
	$(ClassStr).css("margin-left",(i-minHNum)*6 + "px");
}

根据得到的标题数组创建动态的<li>标签,根据最大和最小使用到的H标签动态分配margin值模拟目录之间的包含关系

由于滚动展开的UI,需要给非最高使用到的H标签(即此时目录的非最高级)加上display:none样式,代码中的‘unset’的类名就是这个样式

锚点滚动的实现是因为在hCtn中创建li的顺序和标题顺序一致,通过点击的<li>标签在所有的同类标签中的索引值($.index()函数)确定所对应H标签的元素,从而得到其offsetTop,再通过动画修改scrollTop即可。

在这里将创建的li标签push入$liCtn数组中

至此,如果单纯想要实现目录和目录锚点滚动的UI则无需实现下面的算法了。

树模拟与路径确定与滚动展开UI

//catelog UI
$(window).scroll(function(e){
	var $scrollTop = $(window).scrollTop();
	var $elemTop = $('.catelog-ctn').offset().top;
	//当顶端接触锁定
	if(($scrollTop + 20) >= $elemTop){
		$('.catelog-ctn').css({
			"position": "fixed",
			"top": "20px"
		});
	}
	//解除锁定
	if(($scrollTop + 20) <= 225){
		$('.catelog-ctn').css({
			"position": "absolute",
			"top": "225px"
		});
	}
	//当标题接触浏览器顶端时标红对应条目
	//用索引值寻找对应元素
	//找到最接近顶端的同级元素
	//标红元素路径
	var $flagElem = void 0;
	var $ansElem = void 0;
	$wayList.length = 0;
	//记录最接近的元素
	$hCtn.forEach(function(item,idx){
		var $headElemTop = $(item).offset().top;
		if(($scrollTop + 10) >= $headElemTop){
		$flagElem = item;
		$ansElem = idx;
		}
		if(item.nodeName != $minH)$liCtn[idx].addClass('unset');
	});
	//寻找路径
	//确定更高级元素的最接近位置
	if($flagElem != null && $ansElem != null) {
		findListWay($hCtn,$flagElem,$ansElem);
	}
	//console.log($wayList);
	$('.catalogitem').removeClass('active');
	//标红路径
	for(var i = 0 ; i < $wayList.length ; i++){
		$wayList[i].addClass('active');
		//寻找展开队列,即路径下的所有元素的下一级
		var $idx = $('.catalogitem').index($wayList[i]);
		findSetWay($idx,$hCtn,$liCtn);
	}
});

首先,在无需标红路径的情况下,标红的触发条件是li标签对应的H标签刚好触碰(经过,在其之上)浏览器可视区域顶端的情况。所以这个UI的实现完全在滚动监听($(window).scroll())中实现。

我们能得到的最直观的信息则是最接近顶端的元素,所以先获取该元素,再通过算法溯回出他在树上到根节点的路径则可得到需要标红的路径。

代码中用$flagElem记录最接近的元素,用$ansElem记录该元素在所有H节点中的索引值(即在hCtn中的位置)

函数findListWay()则是寻找路径的函数,代码如下:

//寻找的序列,最接近的元素,该元素索引值
function findListWay($hCtn,$flagElem,$ansElem){
	//加入自身
	$wayList.push($liCtn[$ansElem]);
	for(var i = $ansElem ; i > -1 ; i--){
		if($hCtn[i].nodeName < $flagElem.nodeName){
			findListWay($hCtn,$hCtn[i],i);
			return;
		}
	}
	return;
}

在顺序结构中,由于存在HX->HN(N>X)必然有在树上HX为HN父节点的情况(即若存在H2 H3 则必有H2->H3的树枝),所以也必然满足 对所有节点 最靠近该节点的比他更高级的节点必然是他的父节点(举例:H1 H2 H2-2 H3 ,则到H3的路径是H1->H2-2->H3),根据该规则,只需要从$flagElem出发找到并记录第一个比他高级的节点,再将该节点递推找到下一个更高最近节点,直到循环结束退出递推,得到路径数组$wayList

至此每次滚动都进行一次路径寻找,再通过$wayList将得到的<li>节点标红,则实现了UI-路径标红。

函数findSetWay()则是寻找展开路径的函数,代码如下


//寻找展开队列,即路径下的所有元素的下一级
function findSetWay(num,$hCtn,$liCtn){
	var $checkNum = $hCtn[num].nodeName.slice(1,2);
	//展开
	$liCtn[num].removeClass('unset');
	//按照规则,只有在未遇到H2判断时能遇到H1 H3且并非H1 H2 H3的情况,则迭代最接近层级
	var $lessHnum = 0;
	if($hCtn[num+1]!=null)$lessHnum = $hCtn[num+1].nodeName.slice(1,2);
	for(var i = num + 1 ; i < $hCtn.length ; i++){
		if($hCtn[i] == null) break;
		var $Hnum = $hCtn[i].nodeName.slice(1,2);
		if($lessHnum > $Hnum)$lessHnum = $Hnum;
		console.log($lessHnum);
		//只有在等于最接近层级的时候 默认为下属第一层
		if($Hnum > $checkNum && $lessHnum == $Hnum){
			$liCtn[i].removeClass('unset');
		}
	}
}

UI原理是 若最靠近浏览器可视范围顶端的元素(即$flagElem)在树上有下一级的子节点,则将下一层子节点全部展开(display:block),同样,展开他在目录路径中所有父节点的下属节点。

如这样。

为了达到这个UI,则每次滚动都要将除使用到的最顶级H节点(最顶级的是不会也不应该被隐藏的)对应的<li>标签全部设置为display:none(前面已经设置过了),再通过算法确认哪些<li>标签是可显示的

由于树上存在(H2 H3 H4,这里H4和H3都比H2的层级要小,但是H4不是H2的下一层子节点)的情况,所以不能用单纯的比H2小的的方式寻找展开序列

由于存在(H2 H4,即H2的下一级不是H3的情况)所以不能单纯用比顶级节点小一个层级的方式(即HX-1)寻找展开序列

存在特殊情况(H2 H4 H3)此时H4和H3在树上都为H2的下一层子节点,但是根据上文提到的 对所有节点 最靠近该节点的比他更高级的节点必然是他的父节点 的规则,则可引出 对该顺序结构中 一旦出现HX层级的节点 则其后的HN(N>X)层级的节点在树上必定与HX不属于同一层

所以只需要迭代遇到的最接近父节点(即所需搜寻下属节点的节点)H层级的节点($lessHnum),若其后的节点比该节点的H层级还要高,则迭代该$lessHnum,若其后节点与$lessHnum的层级相同,则认为是该父节点的下一层,加入可显示的队列

当然,这里需要得到路径序列,对路径序列中的所有路径节点进行下属节点的判断,则可完成该UI。


至此所有的UI和算法都已经实现了。

作者已在git上封装了插件,但还是在这里把这一部分完整代码直接贴上来,有需要的可以根据上面的解析稍微进行修改

//目录检测
var $articleElem = $('.article-body-text');
var $articleChildElem = $articleElem.children();
var hReg = /^h/i;
var $hCtn = [];
var $liCtn = [];
var $wayList = [];
var $setList = [];
//鉴定存在的最高级H,最低级H
//实际上推荐只使用H3 H4 H5
var $minH = "H6";
var $maxH = "H1";
for(var i = 0 ; i < $articleChildElem.length ; i++){
	if(hReg.test($articleChildElem[i].nodeName)){
		$hCtn.push($articleChildElem[i]);
		if($minH > $articleChildElem[i].nodeName){
			$minH = $articleChildElem[i].nodeName;
		}
		if($maxH < $articleChildElem[i].nodeName){
			$maxH = $articleChildElem[i].nodeName;
		}
	}
}
//目录排序,提取
//实际上用margin模拟排序,而不是真的有顺序
var minHNum = parseInt($minH.slice(1,2));
var maxHNum = parseInt($maxH.slice(1,2));
var $ulElem = $("<ul class='catalog'></ul>");
//点击事件绑定以及抽取标题文字
$hCtn.forEach(function(item){
	var $liElem = $("<li class='catalogitem'></li>")
	$liElem.addClass(item.nodeName + "item");
	$liElem.text(item.innerHTML);
	$liElem.on('click',function(e){
		var $elem = e.target;
		var $idx = $('.catalogitem').index(e.target);
		console.log($idx);
		var $elemTop = $hCtn[$idx].offsetTop;
		$('html,body').animate({
			scrollTop : $elemTop + "px"
		},500);
	});
	if(item.nodeName != $minH)$liElem.addClass('unset');
	//存储liElem
	$liCtn.push($liElem);
	$ulElem.append($liElem);
});
//插入至容器
$('.catelog-ctn').append($ulElem);
for(var i = maxHNum+1 ; --i ; i > minHNum){
	var ClassStr = ".H" + i + "item";
	$(ClassStr).css("margin-left",(i-minHNum)*6 + "px");
}

//catelog UI
$(window).scroll(function(e){
	var $scrollTop = $(window).scrollTop();
	var $elemTop = $('.catelog-ctn').offset().top;
	//当顶端接触锁定
	if(($scrollTop + 20) >= $elemTop){
		$('.catelog-ctn').css({
			"position": "fixed",
			"top": "20px"
		});
	}
	//解除锁定
	if(($scrollTop + 20) <= 225){
		$('.catelog-ctn').css({
			"position": "absolute",
			"top": "225px"
		});
	}
	//当标题接触浏览器顶端时标红对应条目
	//用索引值寻找对应元素
	//找到最接近顶端的同级元素
	//标红元素路径
	var $flagElem = void 0;
	var $ansElem = void 0;
	$wayList.length = 0;
	//记录最接近的元素
	$hCtn.forEach(function(item,idx){
		var $headElemTop = $(item).offset().top;
		if(($scrollTop + 10) >= $headElemTop){
		$flagElem = item;
		$ansElem = idx;
		}
		if(item.nodeName != $minH)$liCtn[idx].addClass('unset');
	});
	//寻找路径
	//确定更高级元素的最接近位置
	if($flagElem != null && $ansElem != null) {
		findListWay($hCtn,$flagElem,$ansElem);
	}
	//console.log($wayList);
	$('.catalogitem').removeClass('active');
	//标红路径
	for(var i = 0 ; i < $wayList.length ; i++){
		$wayList[i].addClass('active');
		//寻找展开队列,即路径下的所有元素的下一级
		var $idx = $('.catalogitem').index($wayList[i]);
		findSetWay($idx,$hCtn,$liCtn);
	}
});
//寻找的序列,最接近的元素,该元素索引值
function findListWay($hCtn,$flagElem,$ansElem){
	//加入自身
	$wayList.push($liCtn[$ansElem]);
	for(var i = $ansElem ; i > -1 ; i--){
		if($hCtn[i].nodeName < $flagElem.nodeName){
			findListWay($hCtn,$hCtn[i],i);
			return;
		}
	}
	return;
}
//寻找展开队列,即路径下的所有元素的下一级
function findSetWay(num,$hCtn,$liCtn){
	var $checkNum = $hCtn[num].nodeName.slice(1,2);
	//展开
	$liCtn[num].removeClass('unset');
	//按照规则,只有在未遇到H2判断时能遇到H1 H3且并非H1 H2 H3的情况,则迭代最接近层级
	var $lessHnum = 0;
	if($hCtn[num+1]!=null)$lessHnum = $hCtn[num+1].nodeName.slice(1,2);
	for(var i = num + 1 ; i < $hCtn.length ; i++){
		if($hCtn[i] == null) break;
		var $Hnum = $hCtn[i].nodeName.slice(1,2);
		if($lessHnum > $Hnum)$lessHnum = $Hnum;
		console.log($lessHnum);
		//只有在等于最接近层级的时候 默认为下属第一层
		if($Hnum > $checkNum && $lessHnum == $Hnum){
			$liCtn[i].removeClass('unset');
		}
	}
}

作者的github https://github.com/xxx407410849/

求星星,什么项目的星星都可以,本作品已封装插件,请到文章开始处查看git地址

猜你喜欢

转载自blog.csdn.net/u012312705/article/details/81437727