博客园装饰——(三)博客园导航目录
一、功能描述
初始位置在左下角
鼠标放上按钮会显示箭头(目录 展开方向 与 收起方向 ),移开会显示 “目录” 两个字
左键按住按钮,可进行拖拽,且会根据目录的整体大小自动调整在窗口内能拖动到的区域
右键点击按钮,可展开或收起目录
点击目录内的级别高的条目,可将同级的其他条目内的子条目收起,并展开或收起自己内部的低级别的子条目。
点击目录内的条目,可自动导航到其所在的页面内的绝对位置(高度)
1.1 粗略演示
二、 核心算法思想
2.1 假设页面情况
2.1.1 文字描述
先模拟博客园的文章中的标题环境:
- 二、三、四级标题可能是 连续邻近的
- 或者可能它们之间会 穿插很多其他标签及内容
2.1.2 代码描述
<div id="cnblogs_post_body">
<h2>h2-1</h2>
<img src="../01-html文档结构和常用标签/image/臭鼬.jpg" alt="">
<h3>h3-1</h3>
<h4>h4-1</h4>
<a href="">花Q</a>
<h3>h3-1</h3>
<h4>h4-1</h4>
<h3>h3-1</h3>
<h2>h2-2</h2>
<span>街头霸王</span>
<h3>h3-2</h3>
<h3>h3-2</h3>
<h3>h3-2</h3>
<h4>h4-2</h4>
<div>DD斩首</div>
<h4>h4-2</h4>
<h2>h2-3</h2>
<h3>h3-3</h3>
<span>壁纸</span>
<h4>h4-3</h4>
<h3>h3-3</h3>
<img src="../01-html文档结构和常用标签/image/17.jpg" alt="">
<h4>h4-3</h4>
<h3>h3-3</h3>
<h4>h4-3</h4>
</div>
2.1.3 效果图片展示
2.1.4 问题描述
正如上图所示,模拟出的文章环境中,夹着其他很多非二、三、四级标题标签,而我们需要从中 过滤筛选 出标题标签,才能进一步自动生成目录的条目或结构。
2.1.5 解决办法
var oH = $('#cnblogs_post_body').find('h2,h3,h4');
2.2 目录结构
2.2.1 代码描述
- ul 是2,3,4级标题对应的目录条目的容器(用于展开或收起)
- span 才是我们到时,目录可点击的并产生相应事件的条目(用于导航或触发条目的展开或收起)
<div class="diy_menu clearfix">
<ul class="level2_con">
<li>
<!-- 第一个H2 -->
<span id="0" class="level2">h2-1</span>
<ul class="level3_con">
<li>
<!-- 第一个H3 -->
<span id="1" class="level3">h3-1</span>
<ul class="level4_con">
<li>
<!-- 第一个H3中的第一个H4 -->
<span id="2" class="level4">h4-1</span>
</li>
</ul>
</li>
<li>
<!-- 第二个H3 -->
<span id="3" class="level3">h3-1</span>
<ul class="level4_con">
<li>
<!-- 第二个H3中的第一个H4 -->
<span id="4" class="level4">h4-1</span>
</li>
</ul>
</li>
<li>
<!-- 第三个H3 -->
<span id="5" class="level3">h3-1</span>
<ul class="level4_con">
<!-- 第三个H3中没有H4 -->
</ul>
</li>
</ul>
</li>
<li>
<!-- 第二个H2 -->
<span id="6">h2-2</span>
<ul>
.
.
.
</ul>
</li>
.
.
.
</ul>
</div>
2.2.2 图片效果展示
2.2.3 问题引入
为了,以及实现自动根据页面的文本结构自动生成这样的目录结构,当然不可能手动编写html,来生成目录,所以我们需要借助js来识别文章里的标题标签,并通过算法写入,改变文档流,从而生成相应的目录结构。
<!-- 即往这个初始结构中,自动写入html代码 -->
<div class="diy_menu clearfix">
<ul class="level2_con">
</ul>
</div>
2.3 ※ (前期准备)目录生成算法
2.3.1 图文描述
2.3.2 代码描述
// 过滤筛选出h2,h3,h4的标签元素
var oH = $('#cnblogs_post_body').find('h2,h3,h4');
var tagName_list = [];
var tagHigh_list = [];
// 相当于python中的for循环遍历;元素集才可以使用each()函数,列表不能
oH.each(function(i){
tagName_list.push($(this).prop('tagName'));
tagHigh_list.push(Math.ceil($(this).offset().top)); // 得到的高度值进行向上取整
})
// console.log(tagName_list);
// console.log(tagHigh_list);
2.4 ※ (核心)目录生成算法
2.4.1 文字描述
- 我们最核心的思想就是,从而在相应的位置插入相应的级别条目
- 正因为我们每一次循环需要判断它是第几级标签,故我们需要一个列表 tagName_list ,将 "oH" 元素集对应的标签名存起来
- 同时我们可以看出来,文章中的 标题标签顺序 与 tagName_list 里的标签名的,所以我们可以配合 ,实现自动识别并写入不同级别的条目,从而构成一个目录,同时也从侧面反映出我们设计一个 **tagName_list 列表 **的初衷
- eleh2_3 和 eleh4 用于控制写入位置(即节点)
2.4.2 代码描述
var itagName_list = tagName_list.length;
var eleh2_3 = undefined; // 用于插入2级或3级标题标签对应的条目的节点
var eleh4 = undefined; // 用于插入4级标题标签对应的条目的节点
for(var i=0;i<itagName_list;i++){
var a = tagName_list[i];
if (a == 'H2'){
eleh2_3 = $('.level2_con'); // 获取用于插入2级标题标签对应的条目的节点
var oh2 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level2">'+ oh2.html() +'</span><ul class="level3_con"></ul></li>';
eleh2_3.append(plus);
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3 = $('.level2_con>li:last ul'); //选择最后一个即创建的最新的一个li,获取用于插入3级标题标签对应的条目的节点
// console.log(eleh2_3.html());
}
else if( a == 'H3'){
var oh3 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level3">' + oh3.html() + '</span><ul class="level4_con"></ul></li>';
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3.append(plus);
eleh4 = eleh2_3.children("li:last-child").children('ul'); // 获取用于插入4级标题标签对应的条目的节点
// console.log(eleh4.html());
}
else if( a == 'H4'){
var oh4 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level4">' + oh4.html() + '</span></li>';
// eleh4.html(eleh4.html() + plus);
eleh4.append(plus);
}
}
2.5 ※ 目录生成算法总览
/*------------------------------------目录:1.读取页面内的标签并生成目录----------------------------------*/
// 过滤筛选出h2,h3,h4的标签元素
var oH = $('#cnblogs_post_body').find('h2,h3,h4');
var tagName_list = [];
var tagHigh_list = [];
// 相当于python中的for循环遍历;元素集才可以使用each()函数,列表不能
oH.each(function(i){
tagName_list.push($(this).prop('tagName'));
tagHigh_list.push(Math.ceil($(this).offset().top)); // 得到的高度值进行向上取整
})
// console.log(tagName_list);
// console.log(tagHigh_list);
var itagName_list = tagName_list.length;
var eleh2_3 = undefined; // 用于插入2级或3级标题标签对应的条目的节点
var eleh4 = undefined; // 用于插入4级标题标签对应的条目的节点
/*---------目录自动生成算法-------------*/
for(var i=0;i<itagName_list;i++){
var a = tagName_list[i];
if (a == 'H2'){
eleh2_3 = $('.level2_con'); // 获取用于插入2级标题标签对应的条目的节点
var oh2 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level2">'+ oh2.html() +'</span><ul class="level3_con"></ul></li>';
eleh2_3.append(plus);
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3 = $('.level2_con>li:last ul'); // 选择最后一个即创建的最新的一个li,获取用于插入3级标题标签对应的条目的节点
// console.log(eleh2_3.html());
}
else if( a == 'H3'){
var oh3 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level3">' + oh3.html() + '</span><ul class="level4_con"></ul></li>';
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3.append(plus);
eleh4 = eleh2_3.children("li:last-child").children('ul'); // 获取用于插入4级标题标签对应的条目的节点
// console.log(eleh4.html());
}
else if( a == 'H4'){
var oh4 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level4">' + oh4.html() + '</span></li>';
// eleh4.html(eleh4.html() + plus);
eleh4.append(plus);
}
}
2.6 ※ 目录内条目的展开收起与自动导航
/*------------------------目录:2.目录内的条目展开和收起的动画-------------------------------*/
/*-------------利用事件委托,减少相同元素绑定相同事件的次数,提高性能---------------------*/
var olevel2_con = $('.level2_con');
olevel2_con.delegate('.level2, .level3, .level4', 'click', function(){
// console.log($(this).prop('id').substring(8));
// 滚动到相应标签位置
var tagHighIndex = $(this).prop('id').substring(8);
var tagHigh = tagHigh_list[tagHighIndex]; // 取该条目对应的标签的高度值
$('html,body').stop().animate({'scrollTop':tagHigh-250},1000);
// 目录自动合上与展开动画
if ( $(this).prop('className') == 'level2' ){
$(this).parent().siblings().children('.level3_con').children('li').children('.level4_con').stop().slideUp();
$(this).next().stop().slideToggle().parent().siblings().children('ul').stop().slideUp();
}
else if ( $(this).prop('className') == 'level3' ){
//当前点击的元素紧挨的同辈元素向下展开,再跳到此元素的父级(li),再跳到此父级的其他的同辈元素(li),
//选择其他同辈元素(li)的子元素ul,然后将它向上收起。
// 通过stop() 可以修正反复点击导致的持续动画的问题
$(this).next().stop().slideToggle().parent().siblings().children('ul').stop().slideUp();
}
// console.log($(this).prop('className'));
})
三、完整代码展示
前言
3.1 HTML 部分
<body>
<div id="cnblogs_post_body">
<h2>h2-1</h2>
<img src="../01-html文档结构和常用标签/image/臭鼬.jpg" alt="">
<h3>h3-1</h3>
<h4>h4-1</h4>
<a href="">花Q</a>
<h3>h3-1</h3>
<h4>h4-1</h4>
<h3>h3-1</h3>
<h2>h2-2</h2>
<span>街头霸王</span>
<h3>h3-2</h3>
<h3>h3-2</h3>
<h3>h3-2</h3>
<h4>h4-2</h4>
<div>DD斩首</div>
<h4>h4-2</h4>
<h2>h2-3</h2>
<h3>h3-3</h3>
<span>壁纸</span>
<h4>h4-3</h4>
<h3>h3-3</h3>
<img src="../01-html文档结构和常用标签/image/17.jpg" alt="">
<h4>h4-3</h4>
<h3>h3-3</h3>
<h4>h4-3</h4>
</div>
<div class="totop"><span>↑</span></div>
<div class="tobottom"><span>↓</span></div>
<div class="diy_menu clearfix">
<input id="diyMenu_btn" type="button" value="→" title="左键按住我,可以进行拖拽哟~右键点击可以展开哟~">
<ul class="level2_con" title="左键按住左边的按钮可进行拖拽哟~右键点击可以隐藏哟~">
</ul>
</div>
<!--
<p>文档内容</p>
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
(p{文档内容}+br*10)*20 上面这部分代码重复20遍,为了让页面足够长
-->
</body>
3.2 CSS 部分
body,ul{
margin:0px;
padding:0px;
}
.clearfix:after,.clearfix:before{ content: "";display: table;}
.clearfix:after{ clear:both;}
.clearfix{zoom:1;}
ul{list-style:none;}
.diy_menu{
position:fixed;
bottom:100px;
left:75px;
height: 75px;
/*cursor: pointer;*/
/*background-color: gold;*/
}
#diyMenu_btn{
float:left;
width:75px;
font:bold 50px/75px '黑体';
border:0px;
color:white;
cursor: pointer;
border-radius: 20px;
/*用于去除点击按钮后的提示边框*/
outline: none;
opacity: 0.7;
background-color: #c2c0c0b5;
}
@keyframes color_turn{
form{
background-color: #c2c0c0b5;
}
to{
background-color: #40c8f4;
}
}
.btn_color_turn{
animation:color_turn 750ms ease 2 alternate;
}
.level2_con{
width:650px;
border-radius: 10px;
overflow: hidden;
float:left;
margin:0px;
/* opacity: 0.7;*/
}
.level2_con .level2, .level3_con .level3, .level4_con .level4{
display:block;
width:650px;
height:30px;
line-height:30px;
text-decoration:none;
background-color:#fc6d86;
color:#fff;
font-size:20px;
font-weight: bold;
text-indent:10px;
border-bottom:4px dashed white;
opacity: 0.7;
cursor:pointer;
}
#diyMenu_btn:hover, .level2_con .level2:hover, .level3_con .level3:hover, .level4_con .level4:hover{
opacity: 1;
}
.level3_con .level3{
background-color:#ff8ea2;
color:#1eb21ee0;
font-size:17px;
text-indent:20px;
border-bottom:3px solid #1eb21ee0;
}
.level4_con .level4{
background-color:pink;
color:#2893f0d1;
font-size:14px;
text-indent:30px;
border-bottom:2px dashed #2893f0d1;
}
.level3_con{
display: none;
}
.level4_con{
display: none;
}
3.3 JS 部分
$(function(){
/*------------------------------------目录:1.读取页面内的标签并生成目录----------------------------------*/
// 过滤筛选出h2,h3,h4的标签元素
var oH = $('#cnblogs_post_body').find('h2,h3,h4');
var tagName_list = [];
var tagHigh_list = [];
// 相当于python中的for循环遍历;元素集才可以使用each()函数,列表不能
oH.each(function(i){
// i 为索引值
// console.log(i);
// console.log($(this).index());
tagName_list.push($(this).prop('tagName'));
tagHigh_list.push(Math.ceil($(this).offset().top)); // 得到的高度值进行向上取整
})
// console.log(tagName_list);
// console.log(tagHigh_list);
var itagName_list = tagName_list.length;
var eleh2_3 = undefined; // 用于插入2级或3级标题标签对应的条目的节点
var eleh4 = undefined; // 用于插入4级标题标签对应的条目的节点
/*---------目录自动生成算法-------------*/
for(var i=0;i<itagName_list;i++){
var a = tagName_list[i];
if (a == 'H2'){
eleh2_3 = $('.level2_con'); // 获取用于插入2级标题标签对应的条目的节点
var oh2 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level2">'+ oh2.html() +'</span><ul class="level3_con"></ul></li>';
eleh2_3.append(plus);
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3 = $('.level2_con>li:last ul'); // 选择最后一个即创建的最新的一个li,获取用于插入3级标题标签对应的条目的节点
// console.log(eleh2_3.html());
}
else if( a == 'H3'){
var oh3 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level3">' + oh3.html() + '</span><ul class="level4_con"></ul></li>';
// eleh2_3.html(eleh2_3.html() + plus);
eleh2_3.append(plus);
eleh4 = eleh2_3.children("li:last-child").children('ul'); // 获取用于插入4级标题标签对应的条目的节点
// console.log(eleh4.html());
}
else if( a == 'H4'){
var oh4 = oH.eq(i);
var plus = '<li><span id="tagHigh_' + i + '" class="level4">' + oh4.html() + '</span></li>';
// eleh4.html(eleh4.html() + plus);
eleh4.append(plus);
}
}
/*------------------------目录:2.目录内的条目展开和收起的动画-------------------------------*/
/*-------------利用事件委托,减少相同元素绑定相同事件的次数,提高性能---------------------*/
var olevel2_con = $('.level2_con');
olevel2_con.delegate('.level2, .level3, .level4', 'click', function(){
// console.log($(this).prop('id').substring(8));
// 滚动到相应标签位置
var tagHighIndex = $(this).prop('id').substring(8);
var tagHigh = tagHigh_list[tagHighIndex];
$('html,body').stop().animate({'scrollTop':tagHigh-250},1000);
// 目录自动合上与展开动画
if ( $(this).prop('className') == 'level2' ){
$(this).parent().siblings().children('.level3_con').children('li').children('.level4_con').stop().slideUp();
$(this).next().stop().slideToggle().parent().siblings().children('ul').stop().slideUp();
}
else if ( $(this).prop('className') == 'level3' ){
//当前点击的元素紧挨的同辈元素向下展开,再跳到此元素的父级(li),再跳到此父级的其他的同辈元素(li),
//选择其他同辈元素(li)的子元素ul,然后将它向上收起。
// 通过stop() 可以修正反复点击导致的持续动画的问题
$(this).next().stop().slideToggle().parent().siblings().children('ul').stop().slideUp();
}
// console.log($(this).prop('className'));
})
/*------------------------目录:3.目录整体的展开和隐藏的动画---------------------------------*/
/*---------------左键按住按键可进行目录拖拽,右键点击课展开和收起目录------------------*/
// 获取按钮元素
var menu_btn = $('#diyMenu_btn');
var olevel2 = $('.level2');
// 一开始获取原高度,是为了能够使下面的animate动画顺利展开到原高度
var OriHeight = olevel2_con.height();
/*
一开始是隐藏状态,所以将height和width设为0,CSS部分按照正常显示的宽度设置,
页面是等js加载完了,才会显示页面元素,所以无需担心一开始目录会出现突然消失的现象,
更何况程序运行是一瞬间的事情,肉眼无法分辨,从这个角度思考,也是不用担心。
*/
olevel2_con.css({'height': 0, 'width': 0});
// 为了使目录能在浏览器窗口内拖拽的标志,'0'表示目录隐藏,默认目录隐藏,所以初始值为0
var unfoldFlag = 0;
// 拖拽目录
var diyMenu = $('.diy_menu');
var menu_btnHeight = menu_btn.outerHeight(true);
var menuOriHigh = olevel2_con.outerHeight(true);
var oriColor = menu_btn.css('background-color');
/*-----------------------鼠标放在按钮上会切换为箭头显示--------------------*/
// 获取原来按钮的值
var menuBtnVal = menu_btn.val();
// 将初始显示重新设置为目录(即鼠标未放上去时显示目录)
menu_btn.val('目录');
// 由于目录两字如果使用原大小50px,会显得特别大
menu_btn.css({'font-size':25});
menu_btn.mouseenter(function(){
menu_btn.val(menuBtnVal);
menu_btn.css({'font-size':50});
})
menu_btn.mouseleave(function(){
// 离开按钮前再一次获取当前按钮值,用于下一次显示
menuBtnVal = menu_btn.val();
menu_btn.css({'font-size':25});
menu_btn.val('目录');
})
/*------------------------------------------------------------------------*/
menu_btn.mousedown(function(e) {
// 禁止了右键点击该按钮,会弹出浏览器系统菜单
$(this).bind("contextmenu",function(e){
return false;
});
// 获取目录在页面的绝对位置,'e'代表鼠标对象,'e.pageX' 表示鼠标相对于页面的横向位置
var positionDiv = diyMenu.offset();
var distenceX = e.pageX - positionDiv.left; // 获得鼠标点击位置相对于目录左侧的距离
var distenceY = e.pageY - positionDiv.top; // 获得鼠标点击位置相对于目录顶部的距离
// console.log(e.pageY, positionDiv.top);
// alert(distenceX)
// alert(positionDiv.left);
// console.log(e.which); // '1' 代表左键触发事件,'2' 代表中键,'3' 代表右键
// '3' 是右键,用于展开或隐藏目录
if( e.which == 3){
// 移除变为蓝色的动画效果,使得animation动画得以重复播放
// 放在这里,给计算机足够的反应时间,不会出现bug
menu_btn.removeClass('btn_color_turn');
if ( menu_btn.val() == '→'){
menu_btn.val('←');
// 为了使目录能在浏览器窗口内拖拽的标志,'1'表示目录展开
unfoldFlag = 1;
/*-------解决animate动画无法直接让高度恢复为auto值-----------------*/
olevel2_con.stop().animate({
width:olevel2.width(),
height:30,
},500,function(){
olevel2_con.stop().animate({
height:OriHeight,
},1000,function(){
// 放在里面就会在动画结束后,才进行赋值
olevel2_con.css({'height':'auto'});
})
})
// 由于 animate动画的持续时间,不会影响程序的正常运行(多任务)
// 所以下面这一句会在动画还没结束前,先运行,而由于动画过程会动态改变height的值,所以瞬间'auto'被覆盖掉了
// olevel2_con.css({'height':'auto'});
}
else{
menu_btn.val('→');
// 为了使目录能在浏览器窗口内拖拽的标志,'0'表示目录收起,未展开
unfoldFlag = 0;
olevel2.parent().siblings().children('.level3_con').children('li').children('.level4_con').stop().slideUp();
olevel2.next().stop().slideUp().parent().siblings().children('ul').slideUp();
olevel2_con.stop().animate({
height:30,
},1000,function(){
olevel2_con.stop().animate({
width:0,
height:0,
},500)
})
}
}
// '1'代表左键,所以左键负责拖拽
else if( e.which == 1 ){
// console.log(oriColor);
menu_btn.css({'background-color': 'lightgreen'});
$(document).mousemove(function(e) {
// console.log(e.pageX, e.pageY);
/*
鼠标移动后的横向位置 - 鼠标原位置相对于目录左侧的距离 = 目录横向移动后相对于页面的横向位置
(但不是相对于浏览器窗口的位置,由于没有横向滚动条,所以页面宽度和浏览器宽度一样,就无需减去页面滚动距离)
*/
var x = e.pageX - distenceX;
/*
重点:※ 如何获取元素相对于浏览器窗口的距离?
鼠标移动后的纵向位置 - 鼠标原位置相对于目录顶部的距离 = 目录纵向移动后相对于页面的纵向位置
(但不是相对于浏览器窗口的位置,由于有纵向滚动条,所以要获得相对于浏览器窗口的纵向位置,就必须
减去页面向上滚动的距离,即$(document).scrollTop())
*/
// var y = e.pageY - distenceY; // 获得目录纵向移动后相对于页面的纵向位置(但不是相对于浏览器窗口的位置)
var y = e.pageY - distenceY - $(document).scrollTop();
// console.log($(window).height(), diyMenu.height());
if (x < 0) {
x = 0;
}
else if (x > $(window).width() - diyMenu.outerWidth(true)) {
x = $(window).width() - diyMenu.outerWidth(true);
}
if (y < 0) {
y = 0;
}
else{
// 目录没有展开的情况下
if ( (unfoldFlag == 0) && (y > $(window).height() - menu_btnHeight)){
y = $(window).height() - menu_btnHeight;
}
// 目录展开的情况下
else if( (unfoldFlag == 1) && (y > $(window).height() - olevel2_con.outerHeight(true))){
y = $(window).height() - olevel2_con.outerHeight(true);
}
}
diyMenu.css({
'left': x + 'px',
'top': y + 'px'
});
});
}
$(document).mouseup(function() {
$(document).off('mousemove');
menu_btn.css({'background-color': oriColor});
/*---------------------如何使用 jquery + CSS 实现背景色动画效果?------------------*/
// 不能放到此处,因为离下面那一句太近了,会出现bug,即animation动画只能播放一次
// menu_btn.removeClass('btn_color_turn');
// 右键点击会播放animation换色动画
if (e.which == 3){
menu_btn.addClass('btn_color_turn');
}
});
});
/*----------------------------------------------------------------------------------------------------------*/
})
3.4 效果展示
3.4.1 目录展开收起与导航
3.4.2 拖拽演示(左键按住按钮拖拽)
3.4.3 按钮内容与颜色变化
四、总结与后言
上面我着重讲解了核心算法,是因为实现了目录最核心的两个功能:自动生成条目 与 导航
但并不是说其他功能或设计就没什么难度或不重要,就如窗口区域内拖动这个功能,网上寻找很多案例,都无法达到我的想法与预期,所以博主我绞尽脑汁,通过自己的思考终于解决了这一难题。个人认为关于这一功能的算法讲解与难点的剖析,还是很有探讨价值的,故本人打算用另一篇随笔进行讲解。