ScrollToFixed Navigation Bar for avalonjs
做前端经常有做页面内导航条或者边栏的,一般有三点要求:
- 滚动时固定在顶部
- 点击导航项跳转到相应锚点
- 在滚动到某个区域的时候高亮显示当前区域的导航项
这个东西用得还是挺多的,这里我把它封装成了一个 avalonjs 插件, 名字就叫 fixedfloatingnav 吧(名字一定要长长长长长长的 ==)。 1.0 的版本咱先简单点来哈,就做一个水平的飘在顶上的导航条。 接下来我们就一个问题一个问题的解决之。
ps: demo 页面在->这里<-
pps: 代码托管在 github 上,看->这里<-
pps: avalonjs 插件编写指南请参考->司徒正美的教程<-
实现细节
滚动时固定在顶部
这个功能其实也是当初实现这个插件初衷。
第一我们需要实现插件模板,就叫 fixedFloatingNav.html 吧,module 里面的 fixed 字段来确定导航条是否需要固定。这是最终的模板内容:
27日更新,支持部分自定义的样式
<div class="fixed-floating-nav-panel"
ms-css-height="panelHeight"
ms-css-line-height="px">
<ul class="fixed-floating-nav-bar"
ms-class="fixed-floating-nav-bar-fixed:fixed"
ms-css-height="navBarHeight"
ms-css-line-height="px"
ms-css-top="navRelativeTop">
<li class="fixed-floating-nav-item"
ms-repeat="navItems"
ms-click="scrollToAnchorId(el.anchor)"
ms-class="fixed-floating-nav-item-last:$last"
ms-css-height="navBarHeight"
ms-css-line-height="px">
<a ms-class="fixed-floating-nav-item-active:$index===activeIndex"></a>
</li>
</ul>
</div>
如果这个导航条就在页面最顶部的话,我们可以直接用 css 来把导航条定在头部。
.fixed-floating-nav-bar-fixed {
position: fixed;
top: 0;
left: 0; // 和 right 让宽度为 100%,居中导航条的项目
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 10; // 飘在表面,欣越指出为了兼容早期浏览器,一般把 z-index 设置成整 10
// 很多框架这么干的,等我测试一下
background-color: white; // 默认是透明的,你懂的
box-shadow: 0 0 5px gray; // 加个小阴影哈,好看点
}
但是如果这个条条初始位置偏偏在页面中间,要等它滚动碰到天花板后再飘在头部,那这个功能在当前用纯 css 是做不出来的,我们可以通过监听页面的 onscroll 来判断是否应该把导航条飘起来~
在 avalonjs 插件的 $init
方法里面我们绑定 window 对象的 onscroll
事件来检查对象是否碰到天花板了,然后更新 fixed 字段。
...
// check scroll event to change nav bar
var checkScroll = function() {
// check if nav should fixed to top
var navBar = document.getElementsByClassName("fixed-floating-nav")[0];
var rectNav = navBar.getBoundingClientRect();
vmodel.fixed = rectNav.top < 0;
};
vm.$init = function() {
...
checkScroll = avalon.bind(window, "scroll", checkScroll);
...
};
这里只取了 class 是 fixed-floating-nav
的第一个,页面里的导航一般只有一个,有更多的,自己想办法解决哈,1.0 就是这样。。。
跳转到相应锚点
这个功能一般来说简直不用实现,a 本来就可以定位锚点的嘛!
但是这里我做的是一个单页面应用,前端用了路由定位不同模块。如果用 a 标签自带的定位锚点的功能,location 就发生变化了,这个新的 url 是没有意义的。比如在 /index.html/foo 我要定位的 id 是 bar 的元素,直接跳转后 url 变成了 /index.html#bar,这个新 url 明显是打不开的,因为 bar 元素在 foo 模块里面,不在 index.html 里面。
那就自己跳转吧!模板里面的 scrollToAnchorId 就是干这事的!偷偷说这个函数是从司徒正美的 mmRouter 里面的 mmHistory.js 中借鉴来的,哈哈
//得到页面第一个符合条件的A标签
function getFirstAnchor(list) {
for (var i = 0, el; el = list[i++]; ) {
if (el.nodeName === "A") {
return el
}
}
}
// scroll to view
vm.scrollToAnchorId = function(hash, el) {
var navBar = document.getElementsByClassName("fixed-floating-nav")[0];
el = document.getElementById(hash) || getFirstAnchor(document.getElementsByName(hash));
if (navBar && el) {
if (navBar.offsetTop > el.offsetTop) {
el.scrollIntoView();
} else {
// window.scrollTo(0, el.offsetTop - 40);
// 10.27 更新,支持自定义样式
window.scrollTo(0, el.offsetTop - vmodel.navBarHeight);
}
} else {
window.scrollTo(0, 0)
}
};
有没有看到那个 el.offsetTop - vmodel.navBarHeight
,这个因为有导航条飘在上面,直接跳部分内容会被导航条挡住的。
高亮显示导航项
这个嘛,在滚动的时候判断锚点位置,给特定项目高亮就好了,在 checkScroll
中加上点东西:
10.27 更新,把获取元素搬到了 onscroll
事件外面,首先获取所有合法的锚点:
// find out valid anchors
var findValidAnchors = function() {
vmodel.validAnchorIds.splice(0);
vmodel.validAnchorElems.splice(0);
for (var i = 0; i < vmodel.navItems.length; ++i) {
var hash = vmodel.navItems[i].anchor;
if (!!hash) {
var elem = document.getElementById(hash);
if (elem && elem.getBoundingClientRect().height > 0) {
vmodel.validAnchorIds.push(i);
vmodel.validAnchorElems.push(elem);
}
}
}
};
...
vm.$init = function() {
...
vmodel.navElem = element.getElementsByClassName("fixed-floating-nav-panel")[0];
if (!vmodel.navElem) {
throw new Error("找不到导航条");
}
findValidAnchors();
...
}
然后判断的时候直接取存储下来的值
var checkScroll = function() {
...
// change current active index
var i, elem, activeSet = false;
for (i = 0; i < vmodel.validAnchorElems.length; ++i) {
elem = vmodel.validAnchorElems[i];
if (elem.getBoundingClientRect().top > vmodel.navBarHeight) {
vmodel.activeIndex = i === 0 ? 0 : vmodel.validAnchorIds[i - 1];
activeSet = true;
break;
}
}
if (!activeSet) {
vmodel.activeIndex = vmodel.validAnchorIds[i - 1];
}
...
}
其他
把插件的 $init
, $remove
和默认配置贴出来,大家来抛砖
...
vm.$init = function() {
var pageHTML = options.template;
element.style.display = "none";
element.innerHTML = pageHTML;
avalon.scan(element, [vmodel].concat(vmodels));
element.style.display = "block";
vmodel.navElem = element.getElementsByClassName("fixed-floating-nav-panel")[0];
if (!vmodel.navElem) {
throw new Error("找不到导航条");
}
findValidAnchors();
checkScroll = avalon.bind(window, "scroll", checkScroll);
if (typeof options.onInit === "function") {
options.onInit.call(element, vmodel, options, vmodels);
}
};
vm.$remove = function() {
element.innerHTML = element.textContent = "";
avalon.unbind(window, "scroll", checkScroll);
vmodel.navElem = null;
vmodel.validAnchorIds.splice(0);
vmodel.validAnchorElems.splice(0);
};
...
widget.defaults = {
navItems: [], //@param navItems navigation items
onInit: avalon.noop, //@optMethod onInit(vmodel, options, vmodels) 完成初始化之后的回调,call as element's method
panelHeight: 110,
navBarHeight: 40,
offsetY: 30, // to the top of panel
getTemplate: function(tmpl, opts) {
return tmpl;
}, //@optMethod getTemplate(tpl, opts, tplName) 定制修改模板接口
$author: "[email protected]"
}
12月15日更新
当前的导航栏是直接有 js 生成的,这样的话 js 未加载出来的时候导航栏是不可见的, 这样的情况在网络慢的时候就更加明显了,这就破坏了结构的完整
新的版本面直接在子节点里面找 nav
元素,然后解析出里面的 a
标签里面的内容,
生成导航栏的内容。
用法
用起来超级简单,在模型里面配置项目名字和要跳转的锚点 id 就 OK 了:
{
// ...
fixedfloatingnav: {
navItems: [
{label: "项目一", anchor: "item1"},
{label: "项目二", anchor: "item2"}
]
},
// ...
}
或者你是用 bower
的话
bower install avalon-fixed-floating-nav