// ビュワー
{
const transition = 256;
const viewBox = document.createElement('div');
let indexImg;
let idCurrent;
viewBox.id = 'viewBox';
viewBox.title = 'middle button to close';
viewBox.className = 'hidden';
viewBox.view = ()=>{
viewBox.classList.remove('hidden');
};
viewBox.hide = ()=>{
viewBox.classList.add('hidden');
};
viewBox.insertAdjacentHTML('beforeend','
')
const nav = viewBox.querySelector('.nav');
const close = viewBox.querySelector('.close');
const prev = viewBox.querySelector('.previous');
const next = viewBox.querySelector('.next');
// 中ボタンで閉じる
viewBox.addEventListener('mousedown',(event)=>{
if(event.button !== 1) return;
viewBox.hide();
event.preventDefault();
event.stopPropagation();
},false);
// UIのクリックで閉じる
close.addEventListener('click',(event)=>{
if(event.button !== 0) return;
viewBox.hide();
event.preventDefault();
event.stopPropagation();
},false);
// タイムアウト処理 不要になった要素を時間差で削除
let timer;
let obsolete;
const timeout = ()=>{
if(timer){
clearTimeout(timer);
if(obsolete) obsolete.parentNode.removeChild(obsolete);
timer = null;
obsolete = null;
}
};
// ビュワーに新たに画像をセット 既存画像は削除準備
const setImage = (node,position = 'beforeend')=>{
timeout();
const index = idCurrent = node.index;
const caption = node.alt || node.title;
const frame = viewBox.querySelector('.frame');
obsolete = frame.firstChild;
frame.insertAdjacentHTML(position,`${caption}${index + 1} / ${indexImg.length}`);
const figure = frame.querySelector('figure.ready');
setTimeout(()=>{ figure.classList.remove('ready')});
if(obsolete) obsolete.classList.add('hidden');
timer = setTimeout(timeout,transition);
};
// UIのクリックで前の画像
prev.parentNode.addEventListener('click',(event)=>{
if(event.button !== 0) return;
const index = (idCurrent === 0) ? indexImg.length - 1 : --idCurrent;
setImage(indexImg[index],'afterbegin');
event.preventDefault();
event.stopPropagation();
},false);
// UIのクリックで次の画像
next.parentNode.addEventListener('click',(event)=>{
if(event.button !== 0) return;
const index = (idCurrent === indexImg.length - 1) ? 0 : ++idCurrent;
setImage(indexImg[index]);
event.preventDefault();
event.stopPropagation();
},false);
// 画像ビュワーを準備して画像を開く
const openImage = (node)=>{
const frame = viewBox.querySelector('.frame');
while(frame.firstChild) frame.removeChild(frame.firstChild);
setImage(node);
viewBox.classList.add('image');
viewBox.classList.remove('code');
viewBox.view();
indexImg.forEach((img)=>{
if(img.orgSrc){
img.src = img.orgSrc;
delete img.orgSrc;
}
});
};
// コードビュワーを準備してコードを開く
const openCode = (url,type)=>{
const frame = viewBox.querySelector('.frame');
while(frame.firstChild) frame.removeChild(frame.firstChild);
frame.insertAdjacentHTML('beforeend',`
${url.replace(/^.+\//,'')}`);
viewBox.classList.remove('image');
viewBox.classList.add('code');
viewBox.view();
const figure = frame.querySelector('figure.ready');
setTimeout(()=>{ figure.classList.remove('ready')});
fetch(url).then((res)=>{
if(res.ok) return res.text();
else throw Error(res.status + ':' + res.statusText);
}).then((text)=>{
const code = frame.querySelector('code');
code.appendChild(new Text(text));
code.classList.add(type);
hljs.highlightBlock(code.parentNode);
frame.querySelector('pre').classList.remove('hidden');
}).catch((error)=>{
console.log(error);
frame.querySelector('code').appendChild(new Text(error.message));
frame.querySelector('pre').classList.remove('hidden');
});
}
// clickイベントリスナ
const click = (event)=>{
if(event.button !== 0 || event.shiftKey || event.ctrlKey || event.altKey) return;
const el = event.currentTarget;
// img要素は画像ビュワーで開く
if(el instanceof HTMLImageElement){
openImage(el);
}
// 画像ファイルへのリンクも画像ビュワーで開く
else if(el.imgElement){
openImage(el.imgElement);
}
// ソースコードファイルへのリンクはコードビュワーで開く
else{
openCode(el.href,el.href.replace(/^.+\./,'').toLowerCase());
}
event.preventDefault();
event.stopPropagation();
};
// 非表示用CSS追加
const style = document.createElement('style');
document.head.appendChild(style);
document.styleSheets[document.styleSheets.length - 1].insertRule('#viewBox.hidden{opacity:0;pointer-events:none;}',0);
// ビューボックスの要素をbodyに追加
document.body.appendChild(viewBox);
// Intersection Observerで画面内に入った画像を表示
const observer = new IntersectionObserver((entries,obs)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting){
if(entry.target.orgSrc) entry.target.src = entry.target.orgSrc;
delete entry.target.orgSrc;
obs.unobserve(entry.target);
}
});
},{rootMargin:'200px'});
// pageloadイベントリスナ登録
document.addEventListener('pageload',(event)=>{
viewBox.hide();
if(indexImg) indexImg.forEach((img)=>{observer.unobserve(img)});
indexImg = [];
// img要素の登録
[].forEach.bind(document.querySelectorAll('article img'))((el)=>{
el.index = indexImg.length;
el.style.cursor = 'pointer';
el.viewBox = viewBox;
indexImg.push(el);
// 画像サイズがまだ判明していない場合はダミー画像に置き換え
if(!el.width && !el.height){
el.orgSrc = el.src ;
el.src = '/images/960x540.png';
observer.observe(el);
}
el.addEventListener('click',click,false);
});
// リンクの登録
[].forEach.bind(document.querySelectorAll('article a[href]:not([href*="://"])'))((el)=>{
const ext = (el.href.match(/\/[^/]+\.([^./]+)$/)||[null,''])[1].toLowerCase(); if(!ext) return;
// 画像ファイルのリンクはimg要素を作って登録しておく
if(/^jpe?g|png$/.test(ext)){
const img = new Image();
img.src = el.href;
img.index = indexImg.length;
img.title = el.textContent;
el.imgElement = img;
indexImg.push(img);
}
else if(!/^c|cpp|js|txt$/.test(ext)) return
el.viewBox = viewBox;
el.addEventListener('click',click,false);
});
},false);
}
// 動的遷移 ページの読み込み・履歴
{
const parser = new DOMParser();
let oldURL = document.URL;
const isSame = (url)=>{
return (url.replace(/\#.+$/,'') == oldURL.replace(/\#.+$/,''));
};
// ページの読み込み(履歴も含む)
const loadPage = (url,event)=>{
// 同一ページならロードしない
if(isSame(url)){
// 履歴はスクロール位置を復元
if(event.type == 'popstate'){
if(event.state) window.scrollTo(event.state.scrollX,event.state.scrollY);
}
// ページ移動はページ先頭へスクロール
else{
window.scrollTo(0,0);
// ハッシュがある場合その要素へスクロール(縦のみ)
if(location.hash){
const el = document.querySelector(location.hash);
window.scrollTo(0,el.getBoundingClientRect().top);
}
}
return;
}
const old = document.querySelector('#content');
old.classList.add('hidden');
fetch(url).then((res)=>{
if(res.ok) return res.text();
else throw Error(res.status + ':' + res.statusText);
}).then((text)=>{
const doc = parser.parseFromString(text,'text/html');
const content = doc.querySelector('#content');
// #contentが存在しないなら通常のページ移動 ***異常事態***
if(!content) location = url;
// ページ移動ならスクロール位置を現在の履歴に登録してから新規履歴をpush
if(event.type != 'popstate'){
history.replaceState({scrollX:window.scrollX,scrollY:window.scrollY},'',document.URL);
history.pushState(null,'',url);
}
document.title = doc.title;
content.classList.add('hidden');
old.parentNode.replaceChild(content,old);
document.dispatchEvent(new CustomEvent('pageload')); // pageloadイベントの発火
// 履歴はスクロール位置を復元
if(event.type == 'popstate'){
if(event.state) window.scrollTo(event.state.scrollX,event.state.scrollY);
}
// ページ移動はページ先頭へスクロール
else{
window.scrollTo(0,0);
// ハッシュがある場合その要素へスクロール(縦のみ)
if(location.hash){
const el = document.querySelector(location.hash);
window.scrollTo(0,el.getBoundingClientRect().top);
}
}
content.classList.remove('hidden');
});
}
// popstateイベントで履歴の復元
window.addEventListener('popstate',(event)=>{
History.scrollRestoration = 'manual';
if(location.hash != '#contact'){
loadPage(document.URL,event);
}
oldURL = document.URL;
},false);
window.addEventListener('hashchange',(event)=>{
if(oldURL != event.newURL) oldURL = event.newURL;
},false);
// clickイベントリスナ
const click = (event)=>{
if(event.button !== 0 || event.shiftKey || event.ctrlKey || event.altKey || !this.fetch) return;
event.preventDefault();
loadPage(event.currentTarget.href,event);
oldURL = event.currentTarget.href;
};
// pageloadイベント時に動的遷移させるリンクのクリックにイベントリスナを仕込む
document.addEventListener('pageload',(event)=>{
[].forEach.bind(document.querySelectorAll('a[href]:not([href*="://"]):not([href$="#contact"])'))((el)=>{
if(!el.viewBox) el.addEventListener('click',click,false);
});
},false);
}
// その他細々
{
const gtagElement = document.head.querySelector('script[src^="https://www.googletagmanager.com/gtag/"]');
const GA_TRACKING_ID = gtagElement.src.replace(/^.+id=([^=&]+).*?$/,'$1');
// pageloadイベントリスナ登録
document.addEventListener('pageload',(event)=>{
// Google Analytics
if(location.hostname != 'localhost' && gtag) gtag('config',GA_TRACKING_ID,{page_path:document.URL.replace(/^https?:\/\/[^/]+/,'')});
// code.highlight にコードハイライトを設定
[].forEach.bind(document.querySelectorAll('code.highlight'))((el)=>{
if(hljs) hljs.highlightBlock(el);
});
// tweetが埋め込まれていたらレンダリング用のスクリプトをロード・適用
const tweet = document.querySelector('.twitter-tweet');
if(tweet){
const tw = document.head.querySelector('script[src="//platform.twitter.com/widgets.js"]');
if(tw){
if(twttr) twttr.widgets.load(tweet);
}
else{
const script = document.createElement('script');
script.src = '//platform.twitter.com/widgets.js';
script.charset = 'utf-8';
script.async = true;
document.head.appendChild(script);
}
}
},false);
// 検索に単語が引っ掛からないようにHTML出力では無効化されているウィジェット項目を有効化
[].forEach.bind(document.querySelectorAll('.widget .entry'))((el)=>{
const html = el.innerHTML;
if(!/^$/g,''));
});
// メニューのコンタクトフォームのリンク修正
const navContact = document.querySelector('nav a[href$="#contact"]');
if(navContact) navContact.href = '#contact';
// コンタクトフォーム
const contact = document.querySelector('#contact');
if(contact){
// コンタクトフォームを表示可能に
contact.removeAttribute('style');
// resetでコンタクトフォームを閉じる
contact.addEventListener('reset',(event)=>{history.go(-1)},false);
}
}
// pageloadイベントの発火
document.dispatchEvent(new CustomEvent('pageload'));