Create a gist now

Instantly share code, notes, and snippets.

@twilyze / readme.md
Last active Mar 11, 2017

サイドバーに現在位置を表示して追尾する目次【レスポンシブ対応などテスト】 http://twilyze.hatenablog.jp/entry/hatena-blog-custom4

主な変更点(現状)

まだ決定してないところが多いのでそのつもりで(編集途中のファイルを抜き出してるので上手く動かないかも)

追加

  • レスポンシブ対応
  • アーカイブ・トップページでページ内の記事一覧を表示
  • ウィンドウの縦幅よりも大きくなったらスクロールバーを表示する
  • モジュールタイトルを記事タイトルにする機能追加
  • モジュールタイトルを記事トップへのリンクに(仮)
  • タイトル要素(モジュール名)があれば削除してから追加
  • 横スクロールにくっついてこないように
  • モジュールを最後以外にセットした時の対応
  • 設定変更しやすいように上の方にまとめた

変更

  • CSS .sectionList 削除
  • CSS sectionListSide から sidebar-toc に変更
  • ページ下に固定する時の位置を変更
    • container-inner → main-inner

TODO

  • スクリプト読み込み(実行)のタイミングを変える
  • jQuery無しで作る
  • その他細々
/* 目次(サイドバー) */
#box2-inner {
position: relative;
}
#sidebar-toc {
overflow-y: auto;
}
#sidebar-toc ol {
padding: 0px;
margin: 0px;
list-style-type: none;
}
#sidebar-toc .chapter {
padding-left: 10px;
}
#sidebar-toc .toc-anchor {
color: #7d9ab7;
padding: 2px 0px 2px 4px;
display: block;
}
#sidebar-toc .toc-anchor:hover {
color: #263f5a;
background-color: #efefef;
text-decoration: none;
}
#sidebar-toc .current {
background-color: #efefef;
}
#sidebar-toc::-webkit-scrollbar {
width: 8px;
background: #f1f1f1;
}
#sidebar-toc::-webkit-scrollbar-button {
display: none;
}
#sidebar-toc::-webkit-scrollbar-thumb {
background: #c1c1c1;
}
<!-- 追尾する目次 ver3 test -->
<div id='sidebar-toc'></div>
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.slim.min.js'></script>
<script>
//TODO: タイミング変えたほうがいい気がする
$(window).on('load', function() {
'use strict';
//----------------------
/* ↓設定ここから↓
ENTRY:記事, INDEX:トップ, ARCHIVE:アーカイブ
*/
// 各ページのモジュールタイトル('':空文字で非表示)
// ENTRYは'entry-title'にすると記事タイトル表示
var MODULE_TITLE_ENTRY = '目次';
var MODULE_TITLE_INDEX = 'このページの記事一覧';
var MODULE_TITLE_ARCHIVE = 'このページの記事一覧';
// 各ページの動作オンオフ(true, false)
var PAGE_ENTRY = true;
var PAGE_INDEX = true;
var PAGE_ARCHIVE = true;
// 目次を固定した時の余白(px)
var MARGIN_TOP = 10;
// 処理の開始を遅らせる時間(ミリ秒)
var DELAY_TIME = 0;
// スムーズスクロールにかける時間(ミリ秒)
var SMOOTH_SCROLL_TIME = 400;
// スムーズスクロールのイージング('swing', 'linear')
var SMOOTH_SCROLL_EASE = 'swing';
// 表示する見出しタグ(順番注意)
var HEADLINE_QUERY = ['h3','h4','h5'];
// 見出しがn個以下なら表示しない(0以上)
var HEADLINE_MIN = 1;
// 現在位置取得時に余裕をもたせる(px)
var SCROLL_MARGIN = 50;
// 目次内のスクロールを自動追尾(true, false)
var TOC_INSIDE_SCROLL = true;
// ページ内リンクをブラウザ履歴に追加(true, false)
var HISTORY_PUSH = true;
// ↑設定ここまで↑
//----------------------
(function() {
var $sidebarToc = $('#sidebar-toc');
var $sidebarTocModule = $sidebarToc.parent().parent();
var sidebarTocModule = $sidebarTocModule[0];
var bodyClassList = document.body.classList;
var IS_PAGE_ENTRY = PAGE_ENTRY ? bodyClassList.contains('page-entry') : false;
var IS_PAGE_INDEX = PAGE_INDEX ? bodyClassList.contains('page-index') : false;
var IS_PAGE_ARCHIVE = PAGE_ARCHIVE ? bodyClassList.contains('page-archive') : false;
// トップ・記事・アーカイブ・カテゴリページの時だけ
if ( !(IS_PAGE_ENTRY || IS_PAGE_INDEX || IS_PAGE_ARCHIVE) ) {
$sidebarTocModule.hide();
return;
}
console.log('%c---sidebar toc--- DELAY_TIME:' + DELAY_TIME, 'color:blue');
console.time('sidebarToc render');
var elMainInner = document.getElementById('main-inner');
var elContentList;
// 表示する見出しの一覧とモジュールタイトル取得
if (IS_PAGE_ENTRY)
elContentList = elMainInner.querySelectorAll(HEADLINE_QUERY.join());
else {
elContentList = (function() {
var elems = document.getElementsByClassName('entry-title'), ret = [];
for(var i=0, len=elems.length; i < len; i++)
ret.push(elems[i].children[0]);
return ret;
}());
}
// 見出しがn個以下なら目次を表示しない
if (elContentList.length <= HEADLINE_MIN) {
$sidebarTocModule.hide();
console.log('---sidebar toc hide---');
return;
}
//----------------------
var CLASS_HMT = 'hatena-module-title';
var CLASS_ENTRY = 'entry-title';
var CLASS_CURRENT = 'current';
var CLASS_TRACKING = 'tracking';
var STYLE_DEFAULT = {position: 'static'};
//----------------------
var elHatenaToc = elMainInner.getElementsByClassName('table-of-contents');
var hatenaTocId = [];
var hatenaTocFlg = false;
var list = [];
var currentLevel = 0;
// 目次記法を使ってる時はそちらからIDを取得
if (IS_PAGE_ENTRY && elHatenaToc.length) {
var elHatenaTocAnchor = elHatenaToc[0].getElementsByTagName('a');
var sliceIndex = elHatenaTocAnchor[0].href.split('#')[0].length + 1;
for(var i=0, len=elHatenaTocAnchor.length; i<len; i++) {
hatenaTocId.push(elHatenaTocAnchor[i].href.slice(sliceIndex));
elHatenaTocAnchor[i].addEventListener('click', smoothScroll);
}
hatenaTocFlg = true;
}
// タグの設定
$.each(elContentList, function(i, elem) {
var idName = 'section' + i;
var level = 0;
// a要素作成
if (hatenaTocFlg)
idName = hatenaTocId[i];
else
$(elem).attr('id', idName);
list.push('<li><a href="#' + idName + '" class="toc-anchor">' + $(elem).text() + '</a>');
// 段落作成
for (var j=1, len=HEADLINE_QUERY.length; j<len; j++) {
if (elem.nodeName.toLowerCase() === HEADLINE_QUERY[j]) {
level = j;
break;
}
}
while (currentLevel < level) {
list[i] = '<ol class="chapter">' + list[i];
currentLevel++;
}
while (currentLevel > level) {
list[i] = '</ol></li>' + list[i];
currentLevel--;
}
});
// モジュールタイトルの追加
var sidebarTocTitle;
if (IS_PAGE_ENTRY && MODULE_TITLE_ENTRY !== '') {
//document.getElementsByClassName(CLASS_ENTRY)[0].children[0].textContent
if (MODULE_TITLE_ENTRY === CLASS_ENTRY)
sidebarTocTitle = $('.' + CLASS_ENTRY).children()[0].textContent;
else
sidebarTocTitle = MODULE_TITLE_ENTRY;
}
else if (IS_PAGE_INDEX && MODULE_TITLE_INDEX !== '')
sidebarTocTitle = MODULE_TITLE_INDEX;
else if (IS_PAGE_ARCHIVE && MODULE_TITLE_ARCHIVE !== '')
sidebarTocTitle = MODULE_TITLE_ARCHIVE;
$sidebarTocModule.children('.' + CLASS_HMT).remove();
if (sidebarTocTitle)
$sidebarTocModule.prepend('<div class="' + CLASS_HMT + '"><a href="#main">' + sidebarTocTitle + '</a></div>');
// 目次本体の追加
$sidebarToc.append('<ol>' + list.join('') + '</ol>');
$sidebarTocModule.attr('id', 'sidebar-toc-module');
console.log('%c--add toc--', 'color:blue');
// a要素一覧の取得とスムーズスクロールの設定
var $sidebarTocAnchor = $('a', $sidebarToc.children());
$sidebarTocAnchor.on('click', smoothScroll);
//----------------------
// スクロール用
var $win = $(window);
var $box2 = $('#box2');
var $mainInner = $(elMainInner);
var $tocTitle = $sidebarTocModule.children('.' + CLASS_HMT).eq(0);
var $gh = $('#globalheader-container');
var ghHeight = $gh.outerHeight();
var MODULE_OUTSIDE_HEIGHT = $sidebarTocModule.outerHeight() - $sidebarTocModule.height();
var MODULE_STATIC_HEIGHT = $sidebarTocModule.outerHeight(true);
var TOC_LAST_INDEX = list.length - 1;
var headlineTopList = [];
var scrollRange, scrollFixed, scrollAbsolute, move, tracking;
var ghFixedHeight, marginComp, box2Top, leftMargin;
var tocMaxHeight, tocScrollbar, tocScrollHeight;
var tocAHeight = $sidebarTocAnchor.eq(0).outerHeight();
console.log('tocAHeight:' + tocAHeight);
// 目次をサイドバーの最後に設置してる時はその一つ前、それ以外の時は最後のモジュールを取得
var $lastModule = $('#box2-inner').children().last();
var IS_LAST_MODULE_TOC = $lastModule[0] === $sidebarTocModule[0];
var $guideModule = IS_LAST_MODULE_TOC ? $lastModule.prev() : $lastModule;
//----------------------
//TODO
setTimeout(function() {
// ページ表示時に一度実行しておく
setTrackingPoint();
// ウィンドウのリサイズ操作が終わった時に処理する
var timer;
$win.on('resize.toc', function() {
clearTimeout(timer);
timer = setTimeout(setTrackingPoint, 200);
});
//
console.timeEnd('sidebarToc render');
}, DELAY_TIME);
//----------------------
// 関数式
// 現在位置を表示するためのクラス設定とスクロール処理
var setCurrent = (function() {
var current = -1;
return function(i) {
if (i !== current) {
$sidebarTocAnchor.eq(current).removeClass(CLASS_CURRENT);
$sidebarTocAnchor.eq(i).addClass(CLASS_CURRENT);
if (TOC_INSIDE_SCROLL && tocScrollbar && tracking) {
var currentTop;
if (TOC_LAST_INDEX === i)
currentTop = tocScrollHeight;
else
currentTop = $sidebarTocAnchor.eq(i)[0].getBoundingClientRect().top + $sidebarToc.scrollTop();
$sidebarToc.scrollTop(currentTop - tocMaxHeight);
}
current = i;
}
};
}());
// モジュール固定位置が変わったかチェック
var checkPosition = (function() {
var current = 0;
return function(i, callback) {
if (i === void 0)
return current;
if (i !== current) {
if (callback)
callback(current);
current = i;
return true;
}
};
}());
// スクロールイベントの追加と削除
var setScrollEvent = (function() {
var current = false;
return function(b) {
var event = 'scroll.toc';
if (b !== current) {
if (current)
$win.off(event);
else
$win.on(event, updateScroll);
current = b;
}
};
}());
//----------------------
// 関数宣言
// スムーズスクロールと履歴を設定
function smoothScroll(e) {
var hash = e.currentTarget.hash;
var top = Math.min($(hash).offset().top - ghFixedHeight, scrollRange);
$('html,body').animate({scrollTop: top}, SMOOTH_SCROLL_TIME, SMOOTH_SCROLL_EASE);
if (HISTORY_PUSH) window.history.pushState(null, hash, hash);
return false;
}
// モジュール内スクロールバー表示を設定
function setTocScrollBar(bool) {
$sidebarToc.css({'max-height': bool ? tocMaxHeight : ''});
tocScrollbar = bool;
}
// 目次モジュールの設定
function setTocModuleOption(style, bool) {
$sidebarTocModule.css(style);
$sidebarTocModule.toggleClass(CLASS_TRACKING, bool);
setTocScrollBar(bool);
tracking = bool;
}
// 追尾処理に必要な値を設定
function setTrackingPoint() {
var winHeight = window.innerHeight;
var scrollbarX = winHeight - document.documentElement.clientHeight;
console.log('scrollbarX:' + scrollbarX);
scrollRange = Math.max(document.documentElement.scrollHeight - winHeight, 0);
ghFixedHeight = $gh.css('position') === 'fixed' ? ghHeight : 0;
marginComp = ghFixedHeight + MARGIN_TOP;
leftMargin = $guideModule.offset().left;
tocScrollHeight = $sidebarToc[0].scrollHeight;
tocMaxHeight = winHeight - scrollbarX - marginComp - MODULE_OUTSIDE_HEIGHT - $tocTitle.outerHeight(true);
// 各見出しの位置を保存
$.each(elContentList, function(i, elem) {
headlineTopList[i] = $(elem).offset().top - ghFixedHeight;
});
// 横幅を合わせる
$sidebarTocModule.css({'width': $guideModule.width()});
// サイドバーが横に表示されていない時
// if (window.matchMedia('(min-width:1000px)').matches) {
if ($box2.css('float') === 'none') {
console.log('-scrollEvent:OFF-');
setScrollEvent(false);
checkPosition(0);
setTocModuleOption(STYLE_DEFAULT, false);
$sidebarTocAnchor.removeClass(CLASS_CURRENT);
}
// サイドバーより記事の方が小さい時
else if ($box2.outerHeight() > $('#main').outerHeight()) {
console.log('-scrollEvent:ON- move:OFF');
setScrollEvent(true);
move = false;
checkPosition(0);
setTocModuleOption(STYLE_DEFAULT, false);
updateScroll();
}
else {
console.log('-scrollEvent:ON- move:ON');
setScrollEvent(true);
move = true;
// ウィンドウに固定する位置
scrollFixed = $guideModule.offset().top + $guideModule.outerHeight(true) - ghFixedHeight;
var now = checkPosition() !== 0; // 目次モジュールの[現在]位置が初期位置以外か
if (IS_LAST_MODULE_TOC)
scrollFixed -= MARGIN_TOP; // 目次モジュールの[設置]位置が最後の時
else if (now)
scrollFixed += MODULE_STATIC_HEIGHT;
setTocScrollBar(now);
// 下までスクロールした時にページへ固定する位置
var tocModuleMaxHeight = Math.min(winHeight - marginComp, $sidebarTocModule.outerHeight());
scrollAbsolute = $mainInner.offset().top + $mainInner.outerHeight() - tocModuleMaxHeight - marginComp;
box2Top = $box2.offset().top;
console.log('scrollFixed:' + scrollFixed);
console.log('scrollAbsolute:' + scrollAbsolute);
updateScroll();
}
}
// 現在のスクロール位置をもとに表示を更新
function updateScroll() {
var scrollTop = window.pageYOffset;
var scrollLeft = window.pageXOffset;
// 現在位置のクラス設定
if (scrollTop <= SCROLL_MARGIN) {
setCurrent(0);
}
else if ((scrollRange - scrollTop) <= SCROLL_MARGIN) {
setCurrent(TOC_LAST_INDEX);
}
else {
for (var i = TOC_LAST_INDEX; i >= 0; i--) {
if (scrollTop > headlineTopList[i] - SCROLL_MARGIN) {
setCurrent(i);
break;
}
}
}
// モジュールを追従させる
if (move) {
if (scrollAbsolute < scrollTop) {
// ページに固定(下)
if (checkPosition(2)) {
setTocModuleOption({
position: 'absolute', left: '',
top: scrollAbsolute - box2Top + marginComp + 'px'
}, true);
}
}
else if (scrollFixed < scrollTop) {
// ウィンドウに固定
checkPosition(1, function(current) {
var style = {position: 'fixed', top: marginComp + 'px'};
// サイドバーの最後以外に設置されていて直前が初期位置の時はフェードインさせる
var fade = !IS_LAST_MODULE_TOC && current === 0;
if (fade) style.display = 'none';
setTocModuleOption(style, true);
if (fade) $sidebarTocModule.fadeIn(200);
});
}
else {
// 初期位置
if (checkPosition(0))
setTocModuleOption(STYLE_DEFAULT, false);
}
}
// 横方向のスクロールに合わせる
if (checkPosition() === 1)
sidebarTocModule.style.left = leftMargin - scrollLeft + 'px';
}
}());
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment