博客园导航目录


博客园装饰——(三)博客园导航目录

一、功能描述

  1. 初始位置在左下角

  2. 鼠标放上按钮会显示箭头(目录 展开方向收起方向 ),移开会显示 “目录” 两个字

  3. 左键按住按钮,可进行拖拽,且会根据目录的整体大小自动调整在窗口内能拖动到的区域

  4. 右键点击按钮,可展开或收起目录

  5. 点击目录内的级别高的条目,可将同级的其他条目内的子条目收起,并展开或收起自己内部的低级别的子条目。

  6. 点击目录内的条目,可自动导航到其所在的页面内的绝对位置(高度)

1.1 粗略演示

有现成的目录就在你左下角,可以自己亲自体验一下哟~

二、 核心算法思想

其实我们最核心的问题就是如何自动根据页面内所有的h2、h3、h4标签,生成可分级展开或收起的目录。

其余的功能或外观可以根据自己的需求与喜好进行设计与调整,所以此处重点讲解如何自动生成目录?

2.1 假设页面情况

2.1.1 文字描述

先模拟博客园的文章中的标题环境:

  1. 二、三、四级标题可能是 连续邻近的
  2. 或者可能它们之间会 穿插很多其他标签及内容

2.1.2 代码描述

h2-1

h3-1

h4-1

花Q

h3-1

h4-1

h3-1

h2-2

街头霸王

h3-2

h3-2

h3-2

h4-2

DD斩首

h4-2

h2-3

h3-3

壁纸

h4-3

h3-3

h4-3

h3-3

h4-3

2.1.3 效果图片展示

2.1.4 问题描述

正如上图所示,模拟出的文章环境中,夹着其他很多非二、三、四级标题标签,而我们需要从中 过滤筛选 出标题标签,才能进一步自动生成目录的条目或结构。

2.1.5 解决办法

var oH = $('#cnblogs_post_body').find('h2,h3,h4');

在js中通过上式筛选出h2,h3,h4标签,并组成一个元素集oH

2.2 目录结构

2.2.1 代码描述

  1. ul 是2,3,4级标题对应的目录条目的容器(用于展开或收起)
  2. span 才是我们到时,目录可点击的并产生相应事件的条目(用于导航或触发条目的展开或收起)
  • h2-1
    • h3-1
      • h4-1
    • h3-1
      • h4-1
    • h3-1
  • h2-2
      . . .
  • . . .

2 级标题的目录条目,以 li>span+ul 的结构嵌入外层的

    内部

    3 级标题的目录条目,以 li>span+ul 的结构嵌入外层的

      内部

      4 级标题的目录条目,以 li>span 的结构嵌入外层的

        内部

        2.2.2 图片效果展示

        可以自己对照上面的html的标签结构,确认一下~

        上面的目录,是经过博主调整过样式的,为了方便大家直观清晰的看清目录的结构

        2.2.3 问题引入

        为了适配多种情况(即不同的文章结构),以及实现自动根据页面的文本结构自动生成这样的目录结构,当然不可能手动编写html,来生成目录,所以我们需要借助js来识别文章里的标题标签,并通过算法写入,改变文档流,从而生成相应的目录结构。

        
        

        写入规则参照 2.2.1 下方的描述

        2.3 ※ (前期准备)目录生成算法

        2.3.1 图文描述

        由于我们获得的标题标签元素集 'oH'无法直接表现一个目录结构,以及直观地告诉我们这些标签在页面的绝对高度,所以我们需要两个列表来直观提供的展现出来,同时方便我们操作。

        ① 通过各个标签在列表中的先后顺序展现目录结构

        var tagName_list = [ ]

        ② 各个标签对应的页面的绝对高度

        var tagHigh_list = [ ]

        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 文字描述

        1. 我们最核心的思想就是使用 " if " 语句 判断它是第几级标题标签从而在相应的位置插入相应的级别条目
        2. 正因为我们每一次循环需要判断它是第几级标签,故我们需要一个列表 tagName_list ,将 "oH" 元素集对应的标签名存起来
        3. 同时我们可以看出来,文章中的 标题标签顺序tagName_list 里的标签名的逻辑顺序是一样的,所以我们可以配合 " if " 语句,实现自动识别并写入不同级别的条目,从而构成一个目录,同时也从侧面反映出我们设计一个 **tagName_list 列表 **的初衷
        4. eleh2_3eleh4 用于控制写入位置(即节点)

        2.4.2 代码描述

        var itagName_list = tagName_list.length;
        var eleh2_3 = undefined;  // 用于插入2级或3级标题标签对应的条目的节点
        var eleh4 = undefined;  // 用于插入4级标题标签对应的条目的节点
        
        for(var i=0;i'+ oh2.html() +'
          '; 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 = '
        • ' + oh3.html() + '
          • '; // 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 = '
          • ' + oh4.html() + '
          • '; // 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'+ oh2.html() +'
              '; 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 = '
            • ' + oh3.html() + '
              • '; // 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 = '
              • ' + oh4.html() + '
              • '; // 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'));
                    })
                

                三、完整代码展示

                前言

                以下代码只是demo,用于开发和调试的,HTML部分需要删减才能粘贴到博客园应用

                对于看不懂上面讲解或下面代码的小白或者想要源码进行研究的朋友可以点击下面的链接~

                链接:https://pan.baidu.com/s/1ockPW-6RpyonNWp9D0dSIA

                提取码:wz8q

                ※ 不懂如何食用的小白,可以看看我之前发的博客园装饰教程

                3.1 HTML 部分

                
                	

                h2-1

                h3-1

                h4-1

                花Q

                h3-1

                h4-1

                h3-1

                h2-2

                街头霸王

                h3-2

                h3-2

                h3-2

                h4-2

                DD斩首

                h4-2

                h2-3

                h3-3

                壁纸

                h4-3

                h3-3

                h4-3

                h3-3

                h4-3

                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'+ oh2.html() +'
                  '; 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 = '
                • ' + oh3.html() + '
                  • '; // 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 = '
                  • ' + oh4.html() + '
                  • '; // 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 按钮内容与颜色变化

                    四、总结与后言

                    上面我着重讲解了核心算法,是因为实现了目录最核心的两个功能:自动生成条目导航

                    但并不是说其他功能或设计就没什么难度或不重要,就如窗口区域内拖动这个功能,网上寻找很多案例,都无法达到我的想法与预期,所以博主我绞尽脑汁,通过自己的思考终于解决了这一难题。个人认为关于这一功能的算法讲解与难点的剖析,还是很有探讨价值的,故本人打算用另一篇随笔进行讲解。

                    最后再来一发~

                    链接:https://pan.baidu.com/s/1ockPW-6RpyonNWp9D0dSIA

                    提取码:wz8q

                    ※ 不懂如何食用的小白,可以看看我之前发的博客园装饰教程

                    博主还有其他几篇关于博客园装饰的文章,可供观看哟~