2025-2-26-fixed
This commit is contained in:
124
themes/next/source/js/algolia-search.js
Normal file
124
themes/next/source/js/algolia-search.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* global instantsearch, algoliasearch, CONFIG */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const algoliaSettings = CONFIG.algolia;
|
||||
const { indexName, appID, apiKey } = algoliaSettings;
|
||||
|
||||
let search = instantsearch({
|
||||
indexName,
|
||||
searchClient : algoliasearch(appID, apiKey),
|
||||
searchFunction: helper => {
|
||||
let searchInput = document.querySelector('.search-input');
|
||||
if (searchInput.value) {
|
||||
helper.search();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.pjax && search.on('render', () => {
|
||||
window.pjax.refresh(document.getElementById('algolia-hits'));
|
||||
});
|
||||
|
||||
// Registering Widgets
|
||||
search.addWidgets([
|
||||
instantsearch.widgets.configure({
|
||||
hitsPerPage: algoliaSettings.hits.per_page || 10
|
||||
}),
|
||||
|
||||
instantsearch.widgets.searchBox({
|
||||
container : '.search-input-container',
|
||||
placeholder : algoliaSettings.labels.input_placeholder,
|
||||
// Hide default icons of algolia search
|
||||
showReset : false,
|
||||
showSubmit : false,
|
||||
showLoadingIndicator: false,
|
||||
cssClasses : {
|
||||
input: 'search-input'
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.stats({
|
||||
container: '#algolia-stats',
|
||||
templates: {
|
||||
text: data => {
|
||||
let stats = algoliaSettings.labels.hits_stats
|
||||
.replace(/\$\{hits}/, data.nbHits)
|
||||
.replace(/\$\{time}/, data.processingTimeMS);
|
||||
return `${stats}
|
||||
<span class="algolia-powered">
|
||||
<img src="${CONFIG.root}images/algolia_logo.svg" alt="Algolia">
|
||||
</span>
|
||||
<hr>`;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.hits({
|
||||
container: '#algolia-hits',
|
||||
templates: {
|
||||
item: data => {
|
||||
let link = data.permalink ? data.permalink : CONFIG.root + data.path;
|
||||
return `<a href="${link}" class="algolia-hit-item-link">${data._highlightResult.title.value}</a>`;
|
||||
},
|
||||
empty: data => {
|
||||
return `<div id="algolia-hits-empty">
|
||||
${algoliaSettings.labels.hits_empty.replace(/\$\{query}/, data.query)}
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
cssClasses: {
|
||||
item: 'algolia-hit-item'
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.pagination({
|
||||
container: '#algolia-pagination',
|
||||
scrollTo : false,
|
||||
showFirst: false,
|
||||
showLast : false,
|
||||
templates: {
|
||||
first : '<i class="fa fa-angle-double-left"></i>',
|
||||
last : '<i class="fa fa-angle-double-right"></i>',
|
||||
previous: '<i class="fa fa-angle-left"></i>',
|
||||
next : '<i class="fa fa-angle-right"></i>'
|
||||
},
|
||||
cssClasses: {
|
||||
root : 'pagination',
|
||||
item : 'pagination-item',
|
||||
link : 'page-number',
|
||||
selectedItem: 'current',
|
||||
disabledItem: 'disabled-item'
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
search.start();
|
||||
|
||||
// Handle and trigger popup window
|
||||
document.querySelectorAll('.popup-trigger').forEach(element => {
|
||||
element.addEventListener('click', () => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.querySelector('.search-pop-overlay').classList.add('search-active');
|
||||
document.querySelector('.search-input').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Monitor main search box
|
||||
const onPopupClose = () => {
|
||||
document.body.style.overflow = '';
|
||||
document.querySelector('.search-pop-overlay').classList.remove('search-active');
|
||||
};
|
||||
|
||||
document.querySelector('.search-pop-overlay').addEventListener('click', event => {
|
||||
if (event.target === document.querySelector('.search-pop-overlay')) {
|
||||
onPopupClose();
|
||||
}
|
||||
});
|
||||
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
||||
window.addEventListener('pjax:success', onPopupClose);
|
||||
window.addEventListener('keyup', event => {
|
||||
if (event.key === 'Escape') {
|
||||
onPopupClose();
|
||||
}
|
||||
});
|
||||
});
|
||||
56
themes/next/source/js/bookmark.js
Normal file
56
themes/next/source/js/bookmark.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/* global CONFIG */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
'use strict';
|
||||
|
||||
var doSaveScroll = () => {
|
||||
localStorage.setItem('bookmark' + location.pathname, window.scrollY);
|
||||
};
|
||||
|
||||
var scrollToMark = () => {
|
||||
var top = localStorage.getItem('bookmark' + location.pathname);
|
||||
top = parseInt(top, 10);
|
||||
// If the page opens with a specific hash, just jump out
|
||||
if (!isNaN(top) && location.hash === '') {
|
||||
// Auto scroll to the position
|
||||
window.anime({
|
||||
targets : document.scrollingElement,
|
||||
duration : 200,
|
||||
easing : 'linear',
|
||||
scrollTop: top
|
||||
});
|
||||
}
|
||||
};
|
||||
// Register everything
|
||||
var init = function(trigger) {
|
||||
// Create a link element
|
||||
var link = document.querySelector('.book-mark-link');
|
||||
// Scroll event
|
||||
window.addEventListener('scroll', () => link.classList.toggle('book-mark-link-fixed', window.scrollY === 0));
|
||||
// Register beforeunload event when the trigger is auto
|
||||
if (trigger === 'auto') {
|
||||
// Register beforeunload event
|
||||
window.addEventListener('beforeunload', doSaveScroll);
|
||||
window.addEventListener('pjax:send', doSaveScroll);
|
||||
}
|
||||
// Save the position by clicking the icon
|
||||
link.addEventListener('click', () => {
|
||||
doSaveScroll();
|
||||
window.anime({
|
||||
targets : link,
|
||||
duration: 200,
|
||||
easing : 'linear',
|
||||
top : -30,
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
link.style.top = '';
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
});
|
||||
scrollToMark();
|
||||
window.addEventListener('pjax:success', scrollToMark);
|
||||
};
|
||||
|
||||
init(CONFIG.bookmark.save);
|
||||
});
|
||||
278
themes/next/source/js/local-search.js
Normal file
278
themes/next/source/js/local-search.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/* global CONFIG */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Popup Window
|
||||
let isfetched = false;
|
||||
let datas;
|
||||
let isXml = true;
|
||||
// Search DB path
|
||||
let searchPath = CONFIG.path;
|
||||
if (searchPath.length === 0) {
|
||||
searchPath = 'search.xml';
|
||||
} else if (searchPath.endsWith('json')) {
|
||||
isXml = false;
|
||||
}
|
||||
const input = document.querySelector('.search-input');
|
||||
const resultContent = document.getElementById('search-result');
|
||||
|
||||
const getIndexByWord = (word, text, caseSensitive) => {
|
||||
if (CONFIG.localsearch.unescape) {
|
||||
let div = document.createElement('div');
|
||||
div.innerText = word;
|
||||
word = div.innerHTML;
|
||||
}
|
||||
let wordLen = word.length;
|
||||
if (wordLen === 0) return [];
|
||||
let startPosition = 0;
|
||||
let position = [];
|
||||
let index = [];
|
||||
if (!caseSensitive) {
|
||||
text = text.toLowerCase();
|
||||
word = word.toLowerCase();
|
||||
}
|
||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
||||
index.push({ position, word });
|
||||
startPosition = position + wordLen;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
// Merge hits into slices
|
||||
const mergeIntoSlice = (start, end, index, searchText) => {
|
||||
let item = index[index.length - 1];
|
||||
let { position, word } = item;
|
||||
let hits = [];
|
||||
let searchTextCountInSlice = 0;
|
||||
while (position + word.length <= end && index.length !== 0) {
|
||||
if (word === searchText) {
|
||||
searchTextCountInSlice++;
|
||||
}
|
||||
hits.push({
|
||||
position,
|
||||
length: word.length
|
||||
});
|
||||
let wordEnd = position + word.length;
|
||||
|
||||
// Move to next position of hit
|
||||
index.pop();
|
||||
while (index.length !== 0) {
|
||||
item = index[index.length - 1];
|
||||
position = item.position;
|
||||
word = item.word;
|
||||
if (wordEnd > position) {
|
||||
index.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
hits,
|
||||
start,
|
||||
end,
|
||||
searchTextCount: searchTextCountInSlice
|
||||
};
|
||||
};
|
||||
|
||||
// Highlight title and content
|
||||
const highlightKeyword = (text, slice) => {
|
||||
let result = '';
|
||||
let prevEnd = slice.start;
|
||||
slice.hits.forEach(hit => {
|
||||
result += text.substring(prevEnd, hit.position);
|
||||
let end = hit.position + hit.length;
|
||||
result += `<b class="search-keyword">${text.substring(hit.position, end)}</b>`;
|
||||
prevEnd = end;
|
||||
});
|
||||
result += text.substring(prevEnd, slice.end);
|
||||
return result;
|
||||
};
|
||||
|
||||
const inputEventFunction = () => {
|
||||
if (!isfetched) return;
|
||||
let searchText = input.value.trim().toLowerCase();
|
||||
let keywords = searchText.split(/[-\s]+/);
|
||||
if (keywords.length > 1) {
|
||||
keywords.push(searchText);
|
||||
}
|
||||
let resultItems = [];
|
||||
if (searchText.length > 0) {
|
||||
// Perform local searching
|
||||
datas.forEach(({ title, content, url }) => {
|
||||
let titleInLowerCase = title.toLowerCase();
|
||||
let contentInLowerCase = content.toLowerCase();
|
||||
let indexOfTitle = [];
|
||||
let indexOfContent = [];
|
||||
let searchTextCount = 0;
|
||||
keywords.forEach(keyword => {
|
||||
indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false));
|
||||
indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false));
|
||||
});
|
||||
|
||||
// Show search results
|
||||
if (indexOfTitle.length > 0 || indexOfContent.length > 0) {
|
||||
let hitCount = indexOfTitle.length + indexOfContent.length;
|
||||
// Sort index by position of keyword
|
||||
[indexOfTitle, indexOfContent].forEach(index => {
|
||||
index.sort((itemLeft, itemRight) => {
|
||||
if (itemRight.position !== itemLeft.position) {
|
||||
return itemRight.position - itemLeft.position;
|
||||
}
|
||||
return itemLeft.word.length - itemRight.word.length;
|
||||
});
|
||||
});
|
||||
|
||||
let slicesOfTitle = [];
|
||||
if (indexOfTitle.length !== 0) {
|
||||
let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText);
|
||||
searchTextCount += tmp.searchTextCountInSlice;
|
||||
slicesOfTitle.push(tmp);
|
||||
}
|
||||
|
||||
let slicesOfContent = [];
|
||||
while (indexOfContent.length !== 0) {
|
||||
let item = indexOfContent[indexOfContent.length - 1];
|
||||
let { position, word } = item;
|
||||
// Cut out 100 characters
|
||||
let start = position - 20;
|
||||
let end = position + 80;
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
}
|
||||
if (end < position + word.length) {
|
||||
end = position + word.length;
|
||||
}
|
||||
if (end > content.length) {
|
||||
end = content.length;
|
||||
}
|
||||
let tmp = mergeIntoSlice(start, end, indexOfContent, searchText);
|
||||
searchTextCount += tmp.searchTextCountInSlice;
|
||||
slicesOfContent.push(tmp);
|
||||
}
|
||||
|
||||
// Sort slices in content by search text's count and hits' count
|
||||
slicesOfContent.sort((sliceLeft, sliceRight) => {
|
||||
if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) {
|
||||
return sliceRight.searchTextCount - sliceLeft.searchTextCount;
|
||||
} else if (sliceLeft.hits.length !== sliceRight.hits.length) {
|
||||
return sliceRight.hits.length - sliceLeft.hits.length;
|
||||
}
|
||||
return sliceLeft.start - sliceRight.start;
|
||||
});
|
||||
|
||||
// Select top N slices in content
|
||||
let upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10);
|
||||
if (upperBound >= 0) {
|
||||
slicesOfContent = slicesOfContent.slice(0, upperBound);
|
||||
}
|
||||
|
||||
let resultItem = '';
|
||||
|
||||
if (slicesOfTitle.length !== 0) {
|
||||
resultItem += `<li><a href="${url}" class="search-result-title">${highlightKeyword(title, slicesOfTitle[0])}</a>`;
|
||||
} else {
|
||||
resultItem += `<li><a href="${url}" class="search-result-title">${title}</a>`;
|
||||
}
|
||||
|
||||
slicesOfContent.forEach(slice => {
|
||||
resultItem += `<a href="${url}"><p class="search-result">${highlightKeyword(content, slice)}...</p></a>`;
|
||||
});
|
||||
|
||||
resultItem += '</li>';
|
||||
resultItems.push({
|
||||
item: resultItem,
|
||||
id : resultItems.length,
|
||||
hitCount,
|
||||
searchTextCount
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (keywords.length === 1 && keywords[0] === '') {
|
||||
resultContent.innerHTML = '<div id="no-result"><i class="fa fa-search fa-5x"></i></div>';
|
||||
} else if (resultItems.length === 0) {
|
||||
resultContent.innerHTML = '<div id="no-result"><i class="far fa-frown fa-5x"></i></div>';
|
||||
} else {
|
||||
resultItems.sort((resultLeft, resultRight) => {
|
||||
if (resultLeft.searchTextCount !== resultRight.searchTextCount) {
|
||||
return resultRight.searchTextCount - resultLeft.searchTextCount;
|
||||
} else if (resultLeft.hitCount !== resultRight.hitCount) {
|
||||
return resultRight.hitCount - resultLeft.hitCount;
|
||||
}
|
||||
return resultRight.id - resultLeft.id;
|
||||
});
|
||||
resultContent.innerHTML = `<ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`;
|
||||
window.pjax && window.pjax.refresh(resultContent);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
fetch(CONFIG.root + searchPath)
|
||||
.then(response => response.text())
|
||||
.then(res => {
|
||||
// Get the contents from search data
|
||||
isfetched = true;
|
||||
datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => {
|
||||
return {
|
||||
title : element.querySelector('title').textContent,
|
||||
content: element.querySelector('content').textContent,
|
||||
url : element.querySelector('url').textContent
|
||||
};
|
||||
}) : JSON.parse(res);
|
||||
// Only match articles with not empty titles
|
||||
datas = datas.filter(data => data.title).map(data => {
|
||||
data.title = data.title.trim();
|
||||
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
|
||||
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
|
||||
return data;
|
||||
});
|
||||
// Remove loading animation
|
||||
document.getElementById('no-result').innerHTML = '<i class="fa fa-search fa-5x"></i>';
|
||||
inputEventFunction();
|
||||
});
|
||||
};
|
||||
|
||||
if (CONFIG.localsearch.preload) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (CONFIG.localsearch.trigger === 'auto') {
|
||||
input.addEventListener('input', inputEventFunction);
|
||||
} else {
|
||||
document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
|
||||
input.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
inputEventFunction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle and trigger popup window
|
||||
document.querySelectorAll('.popup-trigger').forEach(element => {
|
||||
element.addEventListener('click', () => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.querySelector('.search-pop-overlay').classList.add('search-active');
|
||||
input.focus();
|
||||
if (!isfetched) fetchData();
|
||||
});
|
||||
});
|
||||
|
||||
// Monitor main search box
|
||||
const onPopupClose = () => {
|
||||
document.body.style.overflow = '';
|
||||
document.querySelector('.search-pop-overlay').classList.remove('search-active');
|
||||
};
|
||||
|
||||
document.querySelector('.search-pop-overlay').addEventListener('click', event => {
|
||||
if (event.target === document.querySelector('.search-pop-overlay')) {
|
||||
onPopupClose();
|
||||
}
|
||||
});
|
||||
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
||||
window.addEventListener('pjax:success', onPopupClose);
|
||||
window.addEventListener('keyup', event => {
|
||||
if (event.key === 'Escape') {
|
||||
onPopupClose();
|
||||
}
|
||||
});
|
||||
});
|
||||
177
themes/next/source/js/motion.js
Normal file
177
themes/next/source/js/motion.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/* global NexT, CONFIG, Velocity */
|
||||
|
||||
if (window.$ && window.$.Velocity) window.Velocity = window.$.Velocity;
|
||||
|
||||
NexT.motion = {};
|
||||
|
||||
NexT.motion.integrator = {
|
||||
queue : [],
|
||||
cursor: -1,
|
||||
init : function() {
|
||||
this.queue = [];
|
||||
this.cursor = -1;
|
||||
return this;
|
||||
},
|
||||
add: function(fn) {
|
||||
this.queue.push(fn);
|
||||
return this;
|
||||
},
|
||||
next: function() {
|
||||
this.cursor++;
|
||||
var fn = this.queue[this.cursor];
|
||||
typeof fn === 'function' && fn(NexT.motion.integrator);
|
||||
},
|
||||
bootstrap: function() {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
|
||||
NexT.motion.middleWares = {
|
||||
logo: function(integrator) {
|
||||
var sequence = [];
|
||||
var brand = document.querySelector('.brand');
|
||||
var image = document.querySelector('.custom-logo-image');
|
||||
var title = document.querySelector('.site-title');
|
||||
var subtitle = document.querySelector('.site-subtitle');
|
||||
var logoLineTop = document.querySelector('.logo-line-before i');
|
||||
var logoLineBottom = document.querySelector('.logo-line-after i');
|
||||
|
||||
brand && sequence.push({
|
||||
e: brand,
|
||||
p: {opacity: 1},
|
||||
o: {duration: 200}
|
||||
});
|
||||
|
||||
function getMistLineSettings(element, translateX) {
|
||||
return {
|
||||
e: element,
|
||||
p: {translateX},
|
||||
o: {
|
||||
duration : 500,
|
||||
sequenceQueue: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function pushImageToSequence() {
|
||||
sequence.push({
|
||||
e: image,
|
||||
p: {opacity: 1, top: 0},
|
||||
o: {duration: 200}
|
||||
});
|
||||
}
|
||||
|
||||
CONFIG.scheme === 'Mist' && logoLineTop && logoLineBottom
|
||||
&& sequence.push(
|
||||
getMistLineSettings(logoLineTop, '100%'),
|
||||
getMistLineSettings(logoLineBottom, '-100%')
|
||||
);
|
||||
|
||||
CONFIG.scheme === 'Muse' && image && pushImageToSequence();
|
||||
|
||||
title && sequence.push({
|
||||
e: title,
|
||||
p: {opacity: 1, top: 0},
|
||||
o: {duration: 200}
|
||||
});
|
||||
|
||||
subtitle && sequence.push({
|
||||
e: subtitle,
|
||||
p: {opacity: 1, top: 0},
|
||||
o: {duration: 200}
|
||||
});
|
||||
|
||||
(CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') && image && pushImageToSequence();
|
||||
|
||||
if (sequence.length > 0) {
|
||||
sequence[sequence.length - 1].o.complete = function() {
|
||||
integrator.next();
|
||||
};
|
||||
Velocity.RunSequence(sequence);
|
||||
} else {
|
||||
integrator.next();
|
||||
}
|
||||
|
||||
if (CONFIG.motion.async) {
|
||||
integrator.next();
|
||||
}
|
||||
},
|
||||
|
||||
menu: function(integrator) {
|
||||
Velocity(document.querySelectorAll('.menu-item'), 'transition.slideDownIn', {
|
||||
display : null,
|
||||
duration: 200,
|
||||
complete: function() {
|
||||
integrator.next();
|
||||
}
|
||||
});
|
||||
|
||||
if (CONFIG.motion.async) {
|
||||
integrator.next();
|
||||
}
|
||||
},
|
||||
|
||||
subMenu: function(integrator) {
|
||||
var subMenuItem = document.querySelectorAll('.sub-menu .menu-item');
|
||||
if (subMenuItem.length > 0) {
|
||||
subMenuItem.forEach(element => {
|
||||
element.style.opacity = 1;
|
||||
});
|
||||
}
|
||||
integrator.next();
|
||||
},
|
||||
|
||||
postList: function(integrator) {
|
||||
var postBlock = document.querySelectorAll('.post-block, .pagination, .comments');
|
||||
var postBlockTransition = CONFIG.motion.transition.post_block;
|
||||
var postHeader = document.querySelectorAll('.post-header');
|
||||
var postHeaderTransition = CONFIG.motion.transition.post_header;
|
||||
var postBody = document.querySelectorAll('.post-body');
|
||||
var postBodyTransition = CONFIG.motion.transition.post_body;
|
||||
var collHeader = document.querySelectorAll('.collection-header');
|
||||
var collHeaderTransition = CONFIG.motion.transition.coll_header;
|
||||
|
||||
if (postBlock.length > 0) {
|
||||
var postMotionOptions = window.postMotionOptions || {
|
||||
stagger : 100,
|
||||
drag : true,
|
||||
complete: function() {
|
||||
integrator.next();
|
||||
}
|
||||
};
|
||||
|
||||
if (CONFIG.motion.transition.post_block) {
|
||||
Velocity(postBlock, 'transition.' + postBlockTransition, postMotionOptions);
|
||||
}
|
||||
if (CONFIG.motion.transition.post_header) {
|
||||
Velocity(postHeader, 'transition.' + postHeaderTransition, postMotionOptions);
|
||||
}
|
||||
if (CONFIG.motion.transition.post_body) {
|
||||
Velocity(postBody, 'transition.' + postBodyTransition, postMotionOptions);
|
||||
}
|
||||
if (CONFIG.motion.transition.coll_header) {
|
||||
Velocity(collHeader, 'transition.' + collHeaderTransition, postMotionOptions);
|
||||
}
|
||||
}
|
||||
if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') {
|
||||
integrator.next();
|
||||
}
|
||||
},
|
||||
|
||||
sidebar: function(integrator) {
|
||||
var sidebarAffix = document.querySelector('.sidebar-inner');
|
||||
var sidebarAffixTransition = CONFIG.motion.transition.sidebar;
|
||||
// Only for Pisces | Gemini.
|
||||
if (sidebarAffixTransition && (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini')) {
|
||||
Velocity(sidebarAffix, 'transition.' + sidebarAffixTransition, {
|
||||
display : null,
|
||||
duration: 200,
|
||||
complete: function() {
|
||||
// After motion complete need to remove transform from sidebar to let affix work on Pisces | Gemini.
|
||||
sidebarAffix.style.transform = 'initial';
|
||||
}
|
||||
});
|
||||
}
|
||||
integrator.next();
|
||||
}
|
||||
};
|
||||
114
themes/next/source/js/next-boot.js
Normal file
114
themes/next/source/js/next-boot.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/* global NexT, CONFIG, Velocity */
|
||||
|
||||
NexT.boot = {};
|
||||
|
||||
NexT.boot.registerEvents = function() {
|
||||
|
||||
NexT.utils.registerScrollPercent();
|
||||
NexT.utils.registerCanIUseTag();
|
||||
|
||||
// Mobile top menu bar.
|
||||
document.querySelector('.site-nav-toggle .toggle').addEventListener('click', () => {
|
||||
event.currentTarget.classList.toggle('toggle-close');
|
||||
var siteNav = document.querySelector('.site-nav');
|
||||
var animateAction = siteNav.classList.contains('site-nav-on') ? 'slideUp' : 'slideDown';
|
||||
|
||||
if (typeof Velocity === 'function') {
|
||||
Velocity(siteNav, animateAction, {
|
||||
duration: 200,
|
||||
complete: function() {
|
||||
siteNav.classList.toggle('site-nav-on');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
siteNav.classList.toggle('site-nav-on');
|
||||
}
|
||||
});
|
||||
|
||||
var TAB_ANIMATE_DURATION = 200;
|
||||
document.querySelectorAll('.sidebar-nav li').forEach((element, index) => {
|
||||
element.addEventListener('click', event => {
|
||||
var item = event.currentTarget;
|
||||
var activeTabClassName = 'sidebar-nav-active';
|
||||
var activePanelClassName = 'sidebar-panel-active';
|
||||
if (item.classList.contains(activeTabClassName)) return;
|
||||
|
||||
var targets = document.querySelectorAll('.sidebar-panel');
|
||||
var target = targets[index];
|
||||
var currentTarget = targets[1 - index];
|
||||
window.anime({
|
||||
targets : currentTarget,
|
||||
duration: TAB_ANIMATE_DURATION,
|
||||
easing : 'linear',
|
||||
opacity : 0,
|
||||
complete: () => {
|
||||
// Prevent adding TOC to Overview if Overview was selected when close & open sidebar.
|
||||
currentTarget.classList.remove(activePanelClassName);
|
||||
target.style.opacity = 0;
|
||||
target.classList.add(activePanelClassName);
|
||||
window.anime({
|
||||
targets : target,
|
||||
duration: TAB_ANIMATE_DURATION,
|
||||
easing : 'linear',
|
||||
opacity : 1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
[...item.parentNode.children].forEach(element => {
|
||||
element.classList.remove(activeTabClassName);
|
||||
});
|
||||
item.classList.add(activeTabClassName);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', NexT.utils.initSidebarDimension);
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
var tHash = location.hash;
|
||||
if (tHash !== '' && !tHash.match(/%\S{2}/)) {
|
||||
var target = document.querySelector(`.tabs ul.nav-tabs li a[href="${tHash}"]`);
|
||||
target && target.click();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
NexT.boot.refresh = function() {
|
||||
|
||||
/**
|
||||
* Register JS handlers by condition option.
|
||||
* Need to add config option in Front-End at 'layout/_partials/head.swig' file.
|
||||
*/
|
||||
CONFIG.fancybox && NexT.utils.wrapImageWithFancyBox();
|
||||
CONFIG.mediumzoom && window.mediumZoom('.post-body :not(a) > img, .post-body > img');
|
||||
CONFIG.lazyload && window.lozad('.post-body img').observe();
|
||||
CONFIG.pangu && window.pangu.spacingPage();
|
||||
|
||||
CONFIG.exturl && NexT.utils.registerExtURL();
|
||||
CONFIG.copycode.enable && NexT.utils.registerCopyCode();
|
||||
NexT.utils.registerTabsTag();
|
||||
NexT.utils.registerActiveMenuItem();
|
||||
NexT.utils.registerLangSelect();
|
||||
NexT.utils.registerSidebarTOC();
|
||||
NexT.utils.wrapTableWithBox();
|
||||
NexT.utils.registerVideoIframe();
|
||||
};
|
||||
|
||||
NexT.boot.motion = function() {
|
||||
// Define Motion Sequence & Bootstrap Motion.
|
||||
if (CONFIG.motion.enable) {
|
||||
NexT.motion.integrator
|
||||
.add(NexT.motion.middleWares.logo)
|
||||
.add(NexT.motion.middleWares.menu)
|
||||
.add(NexT.motion.middleWares.postList)
|
||||
.add(NexT.motion.middleWares.sidebar)
|
||||
.bootstrap();
|
||||
}
|
||||
NexT.utils.updateSidebarPosition();
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
NexT.boot.registerEvents();
|
||||
NexT.boot.refresh();
|
||||
NexT.boot.motion();
|
||||
});
|
||||
113
themes/next/source/js/schemes/muse.js
Normal file
113
themes/next/source/js/schemes/muse.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/* global NexT, CONFIG, Velocity */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
var isRight = CONFIG.sidebar.position === 'right';
|
||||
var SIDEBAR_WIDTH = CONFIG.sidebar.width || 320;
|
||||
var SIDEBAR_DISPLAY_DURATION = 200;
|
||||
var mousePos = {};
|
||||
|
||||
var sidebarToggleLines = {
|
||||
lines: document.querySelector('.sidebar-toggle'),
|
||||
init : function() {
|
||||
this.lines.classList.remove('toggle-arrow', 'toggle-close');
|
||||
},
|
||||
arrow: function() {
|
||||
this.lines.classList.remove('toggle-close');
|
||||
this.lines.classList.add('toggle-arrow');
|
||||
},
|
||||
close: function() {
|
||||
this.lines.classList.remove('toggle-arrow');
|
||||
this.lines.classList.add('toggle-close');
|
||||
}
|
||||
};
|
||||
|
||||
var sidebarToggleMotion = {
|
||||
sidebarEl : document.querySelector('.sidebar'),
|
||||
isSidebarVisible: false,
|
||||
init : function() {
|
||||
sidebarToggleLines.init();
|
||||
|
||||
window.addEventListener('mousedown', this.mousedownHandler.bind(this));
|
||||
window.addEventListener('mouseup', this.mouseupHandler.bind(this));
|
||||
document.querySelector('#sidebar-dimmer').addEventListener('click', this.clickHandler.bind(this));
|
||||
document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this));
|
||||
document.querySelector('.sidebar-toggle').addEventListener('mouseenter', this.mouseEnterHandler.bind(this));
|
||||
document.querySelector('.sidebar-toggle').addEventListener('mouseleave', this.mouseLeaveHandler.bind(this));
|
||||
window.addEventListener('sidebar:show', this.showSidebar.bind(this));
|
||||
window.addEventListener('sidebar:hide', this.hideSidebar.bind(this));
|
||||
},
|
||||
mousedownHandler: function(event) {
|
||||
mousePos.X = event.pageX;
|
||||
mousePos.Y = event.pageY;
|
||||
},
|
||||
mouseupHandler: function(event) {
|
||||
var deltaX = event.pageX - mousePos.X;
|
||||
var deltaY = event.pageY - mousePos.Y;
|
||||
var clickingBlankPart = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)) < 20 && event.target.matches('.main');
|
||||
if (this.isSidebarVisible && (clickingBlankPart || event.target.matches('img.medium-zoom-image, .fancybox img'))) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
},
|
||||
clickHandler: function() {
|
||||
this.isSidebarVisible ? this.hideSidebar() : this.showSidebar();
|
||||
},
|
||||
mouseEnterHandler: function() {
|
||||
if (!this.isSidebarVisible) {
|
||||
sidebarToggleLines.arrow();
|
||||
}
|
||||
},
|
||||
mouseLeaveHandler: function() {
|
||||
if (!this.isSidebarVisible) {
|
||||
sidebarToggleLines.init();
|
||||
}
|
||||
},
|
||||
showSidebar: function() {
|
||||
this.isSidebarVisible = true;
|
||||
this.sidebarEl.classList.add('sidebar-active');
|
||||
if (typeof Velocity === 'function') {
|
||||
Velocity(document.querySelectorAll('.sidebar .motion-element'), isRight ? 'transition.slideRightIn' : 'transition.slideLeftIn', {
|
||||
stagger: 50,
|
||||
drag : true
|
||||
});
|
||||
}
|
||||
|
||||
sidebarToggleLines.close();
|
||||
NexT.utils.isDesktop() && window.anime(Object.assign({
|
||||
targets : document.body,
|
||||
duration: SIDEBAR_DISPLAY_DURATION,
|
||||
easing : 'linear'
|
||||
}, isRight ? {
|
||||
'padding-right': SIDEBAR_WIDTH
|
||||
} : {
|
||||
'padding-left': SIDEBAR_WIDTH
|
||||
}));
|
||||
},
|
||||
hideSidebar: function() {
|
||||
this.isSidebarVisible = false;
|
||||
this.sidebarEl.classList.remove('sidebar-active');
|
||||
|
||||
sidebarToggleLines.init();
|
||||
NexT.utils.isDesktop() && window.anime(Object.assign({
|
||||
targets : document.body,
|
||||
duration: SIDEBAR_DISPLAY_DURATION,
|
||||
easing : 'linear'
|
||||
}, isRight ? {
|
||||
'padding-right': 0
|
||||
} : {
|
||||
'padding-left': 0
|
||||
}));
|
||||
}
|
||||
};
|
||||
sidebarToggleMotion.init();
|
||||
|
||||
function updateFooterPosition() {
|
||||
var footer = document.querySelector('.footer');
|
||||
var containerHeight = document.querySelector('.header').offsetHeight + document.querySelector('.main').offsetHeight + footer.offsetHeight;
|
||||
footer.classList.toggle('footer-fixed', containerHeight <= window.innerHeight);
|
||||
}
|
||||
|
||||
updateFooterPosition();
|
||||
window.addEventListener('resize', updateFooterPosition);
|
||||
window.addEventListener('scroll', updateFooterPosition);
|
||||
});
|
||||
86
themes/next/source/js/schemes/pisces.js
Normal file
86
themes/next/source/js/schemes/pisces.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/* global NexT, CONFIG */
|
||||
|
||||
var Affix = {
|
||||
init: function(element, options) {
|
||||
this.element = element;
|
||||
this.offset = options || 0;
|
||||
this.affixed = null;
|
||||
this.unpin = null;
|
||||
this.pinnedOffset = null;
|
||||
this.checkPosition();
|
||||
window.addEventListener('scroll', this.checkPosition.bind(this));
|
||||
window.addEventListener('click', this.checkPositionWithEventLoop.bind(this));
|
||||
window.matchMedia('(min-width: 992px)').addListener(event => {
|
||||
if (event.matches) {
|
||||
this.offset = NexT.utils.getAffixParam();
|
||||
this.checkPosition();
|
||||
}
|
||||
});
|
||||
},
|
||||
getState: function(scrollHeight, height, offsetTop, offsetBottom) {
|
||||
let scrollTop = window.scrollY;
|
||||
let targetHeight = window.innerHeight;
|
||||
if (offsetTop != null && this.affixed === 'top') {
|
||||
if (document.querySelector('.content-wrap').offsetHeight < offsetTop) return 'top';
|
||||
return scrollTop < offsetTop ? 'top' : false;
|
||||
}
|
||||
if (this.affixed === 'bottom') {
|
||||
if (offsetTop != null) return this.unpin <= this.element.getBoundingClientRect().top ? false : 'bottom';
|
||||
return scrollTop + targetHeight <= scrollHeight - offsetBottom ? false : 'bottom';
|
||||
}
|
||||
let initializing = this.affixed === null;
|
||||
let colliderTop = initializing ? scrollTop : this.element.getBoundingClientRect().top + scrollTop;
|
||||
let colliderHeight = initializing ? targetHeight : height;
|
||||
if (offsetTop != null && scrollTop <= offsetTop) return 'top';
|
||||
if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom';
|
||||
return false;
|
||||
},
|
||||
getPinnedOffset: function() {
|
||||
if (this.pinnedOffset) return this.pinnedOffset;
|
||||
this.element.classList.remove('affix-top', 'affix-bottom');
|
||||
this.element.classList.add('affix');
|
||||
return (this.pinnedOffset = this.element.getBoundingClientRect().top);
|
||||
},
|
||||
checkPositionWithEventLoop() {
|
||||
setTimeout(this.checkPosition.bind(this), 1);
|
||||
},
|
||||
checkPosition: function() {
|
||||
if (window.getComputedStyle(this.element).display === 'none') return;
|
||||
let height = this.element.offsetHeight;
|
||||
let { offset } = this;
|
||||
let offsetTop = offset.top;
|
||||
let offsetBottom = offset.bottom;
|
||||
let { scrollHeight } = document.body;
|
||||
let affix = this.getState(scrollHeight, height, offsetTop, offsetBottom);
|
||||
if (this.affixed !== affix) {
|
||||
if (this.unpin != null) this.element.style.top = '';
|
||||
let affixType = 'affix' + (affix ? '-' + affix : '');
|
||||
this.affixed = affix;
|
||||
this.unpin = affix === 'bottom' ? this.getPinnedOffset() : null;
|
||||
this.element.classList.remove('affix', 'affix-top', 'affix-bottom');
|
||||
this.element.classList.add(affixType);
|
||||
}
|
||||
if (affix === 'bottom') {
|
||||
this.element.style.top = scrollHeight - height - offsetBottom + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NexT.utils.getAffixParam = function() {
|
||||
const sidebarOffset = CONFIG.sidebar.offset || 12;
|
||||
|
||||
let headerOffset = document.querySelector('.header-inner').offsetHeight;
|
||||
let footerOffset = document.querySelector('.footer').offsetHeight;
|
||||
|
||||
document.querySelector('.sidebar').style.marginTop = headerOffset + sidebarOffset + 'px';
|
||||
|
||||
return {
|
||||
top : headerOffset,
|
||||
bottom: footerOffset
|
||||
};
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
Affix.init(document.querySelector('.sidebar-inner'), NexT.utils.getAffixParam());
|
||||
});
|
||||
415
themes/next/source/js/utils.js
Normal file
415
themes/next/source/js/utils.js
Normal file
@@ -0,0 +1,415 @@
|
||||
/* global NexT, CONFIG */
|
||||
|
||||
HTMLElement.prototype.wrap = function(wrapper) {
|
||||
this.parentNode.insertBefore(wrapper, this);
|
||||
this.parentNode.removeChild(this);
|
||||
wrapper.appendChild(this);
|
||||
};
|
||||
|
||||
NexT.utils = {
|
||||
|
||||
/**
|
||||
* Wrap images with fancybox.
|
||||
*/
|
||||
wrapImageWithFancyBox: function() {
|
||||
document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => {
|
||||
var $image = $(element);
|
||||
var imageLink = $image.attr('data-src') || $image.attr('src');
|
||||
var $imageWrapLink = $image.wrap(`<a class="fancybox fancybox.image" href="${imageLink}" itemscope itemtype="http://schema.org/ImageObject" itemprop="url"></a>`).parent('a');
|
||||
if ($image.is('.post-gallery img')) {
|
||||
$imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery');
|
||||
} else if ($image.is('.group-picture img')) {
|
||||
$imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group');
|
||||
} else {
|
||||
$imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default');
|
||||
}
|
||||
|
||||
var imageTitle = $image.attr('title') || $image.attr('alt');
|
||||
if (imageTitle) {
|
||||
$imageWrapLink.append(`<p class="image-caption">${imageTitle}</p>`);
|
||||
// Make sure img title tag will show correctly in fancybox
|
||||
$imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle);
|
||||
}
|
||||
});
|
||||
|
||||
$.fancybox.defaults.hash = false;
|
||||
$('.fancybox').fancybox({
|
||||
loop : true,
|
||||
helpers: {
|
||||
overlay: {
|
||||
locked: false
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
registerExtURL: function() {
|
||||
document.querySelectorAll('span.exturl').forEach(element => {
|
||||
let link = document.createElement('a');
|
||||
// https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
|
||||
link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
link.rel = 'noopener external nofollow noreferrer';
|
||||
link.target = '_blank';
|
||||
link.className = element.className;
|
||||
link.title = element.title;
|
||||
link.innerHTML = element.innerHTML;
|
||||
element.parentNode.replaceChild(link, element);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* One-click copy code support.
|
||||
*/
|
||||
registerCopyCode: function() {
|
||||
document.querySelectorAll('figure.highlight').forEach(element => {
|
||||
const box = document.createElement('div');
|
||||
element.wrap(box);
|
||||
box.classList.add('highlight-container');
|
||||
box.insertAdjacentHTML('beforeend', '<div class="copy-btn"><i class="fa fa-clipboard fa-fw"></i></div>');
|
||||
var button = element.parentNode.querySelector('.copy-btn');
|
||||
button.addEventListener('click', event => {
|
||||
var target = event.currentTarget;
|
||||
var code = [...target.parentNode.querySelectorAll('.code .line')].map(line => line.innerText).join('\n');
|
||||
var ta = document.createElement('textarea');
|
||||
ta.style.top = window.scrollY + 'px'; // Prevent page scrolling
|
||||
ta.style.position = 'absolute';
|
||||
ta.style.opacity = '0';
|
||||
ta.readOnly = true;
|
||||
ta.value = code;
|
||||
document.body.append(ta);
|
||||
const selection = document.getSelection();
|
||||
const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
|
||||
ta.select();
|
||||
ta.setSelectionRange(0, code.length);
|
||||
ta.readOnly = false;
|
||||
var result = document.execCommand('copy');
|
||||
if (CONFIG.copycode.show_result) {
|
||||
target.querySelector('i').className = result ? 'fa fa-check fa-fw' : 'fa fa-times fa-fw';
|
||||
}
|
||||
ta.blur(); // For iOS
|
||||
target.blur();
|
||||
if (selected) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(selected);
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
});
|
||||
button.addEventListener('mouseleave', event => {
|
||||
setTimeout(() => {
|
||||
event.target.querySelector('i').className = 'fa fa-clipboard fa-fw';
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
wrapTableWithBox: function() {
|
||||
document.querySelectorAll('table').forEach(element => {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'table-container';
|
||||
element.wrap(box);
|
||||
});
|
||||
},
|
||||
|
||||
registerVideoIframe: function() {
|
||||
document.querySelectorAll('iframe').forEach(element => {
|
||||
const supported = [
|
||||
'www.youtube.com',
|
||||
'player.vimeo.com',
|
||||
'player.youku.com',
|
||||
'player.bilibili.com',
|
||||
'www.tudou.com'
|
||||
].some(host => element.src.includes(host));
|
||||
if (supported && !element.parentNode.matches('.video-container')) {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'video-container';
|
||||
element.wrap(box);
|
||||
let width = Number(element.width);
|
||||
let height = Number(element.height);
|
||||
if (width && height) {
|
||||
element.parentNode.style.paddingTop = (height / width * 100) + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
registerScrollPercent: function() {
|
||||
var THRESHOLD = 50;
|
||||
var backToTop = document.querySelector('.back-to-top');
|
||||
var readingProgressBar = document.querySelector('.reading-progress-bar');
|
||||
// For init back to top in sidebar if page was scrolled after page refresh.
|
||||
window.addEventListener('scroll', () => {
|
||||
if (backToTop || readingProgressBar) {
|
||||
var docHeight = document.querySelector('.container').offsetHeight;
|
||||
var winHeight = window.innerHeight;
|
||||
var contentVisibilityHeight = docHeight > winHeight ? docHeight - winHeight : document.body.scrollHeight - winHeight;
|
||||
var scrollPercent = Math.min(100 * window.scrollY / contentVisibilityHeight, 100);
|
||||
if (backToTop) {
|
||||
backToTop.classList.toggle('back-to-top-on', window.scrollY > THRESHOLD);
|
||||
backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%';
|
||||
}
|
||||
if (readingProgressBar) {
|
||||
readingProgressBar.style.width = scrollPercent.toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
backToTop && backToTop.addEventListener('click', () => {
|
||||
window.anime({
|
||||
targets : document.scrollingElement,
|
||||
duration : 500,
|
||||
easing : 'linear',
|
||||
scrollTop: 0
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Tabs tag listener (without twitter bootstrap).
|
||||
*/
|
||||
registerTabsTag: function() {
|
||||
// Binding `nav-tabs` & `tab-content` by real time permalink changing.
|
||||
document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => {
|
||||
element.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
var target = event.currentTarget;
|
||||
// Prevent selected tab to select again.
|
||||
if (!target.classList.contains('active')) {
|
||||
// Add & Remove active class on `nav-tabs` & `tab-content`.
|
||||
[...target.parentNode.children].forEach(element => {
|
||||
element.classList.remove('active');
|
||||
});
|
||||
target.classList.add('active');
|
||||
var tActive = document.getElementById(target.querySelector('a').getAttribute('href').replace('#', ''));
|
||||
[...tActive.parentNode.children].forEach(element => {
|
||||
element.classList.remove('active');
|
||||
});
|
||||
tActive.classList.add('active');
|
||||
// Trigger event
|
||||
tActive.dispatchEvent(new Event('tabs:click', {
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
window.dispatchEvent(new Event('tabs:register'));
|
||||
},
|
||||
|
||||
registerCanIUseTag: function() {
|
||||
// Get responsive height passed from iframe.
|
||||
window.addEventListener('message', ({ data }) => {
|
||||
if ((typeof data === 'string') && data.includes('ciu_embed')) {
|
||||
var featureID = data.split(':')[1];
|
||||
var height = data.split(':')[2];
|
||||
document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px';
|
||||
}
|
||||
}, false);
|
||||
},
|
||||
|
||||
registerActiveMenuItem: function() {
|
||||
document.querySelectorAll('.menu-item').forEach(element => {
|
||||
var target = element.querySelector('a[href]');
|
||||
if (!target) return;
|
||||
var isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', '');
|
||||
var isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname);
|
||||
element.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath));
|
||||
});
|
||||
},
|
||||
|
||||
registerLangSelect: function() {
|
||||
let selects = document.querySelectorAll('.lang-select');
|
||||
selects.forEach(sel => {
|
||||
sel.value = CONFIG.page.lang;
|
||||
sel.addEventListener('change', () => {
|
||||
let target = sel.options[sel.selectedIndex];
|
||||
document.querySelectorAll('.lang-select-label span').forEach(span => span.innerText = target.text);
|
||||
let url = target.dataset.href;
|
||||
window.pjax ? window.pjax.loadUrl(url) : window.location.href = url;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
registerSidebarTOC: function() {
|
||||
const navItems = document.querySelectorAll('.post-toc li');
|
||||
const sections = [...navItems].map(element => {
|
||||
var link = element.querySelector('a.nav-link');
|
||||
var target = document.getElementById(decodeURI(link.getAttribute('href')).replace('#', ''));
|
||||
// TOC item animation navigate.
|
||||
link.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
var offset = target.getBoundingClientRect().top + window.scrollY;
|
||||
window.anime({
|
||||
targets : document.scrollingElement,
|
||||
duration : 500,
|
||||
easing : 'linear',
|
||||
scrollTop: offset + 10
|
||||
});
|
||||
});
|
||||
return target;
|
||||
});
|
||||
|
||||
var tocElement = document.querySelector('.post-toc-wrap');
|
||||
function activateNavByIndex(target) {
|
||||
if (target.classList.contains('active-current')) return;
|
||||
|
||||
document.querySelectorAll('.post-toc .active').forEach(element => {
|
||||
element.classList.remove('active', 'active-current');
|
||||
});
|
||||
target.classList.add('active', 'active-current');
|
||||
var parent = target.parentNode;
|
||||
while (!parent.matches('.post-toc')) {
|
||||
if (parent.matches('li')) parent.classList.add('active');
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
// Scrolling to center active TOC element if TOC content is taller then viewport.
|
||||
window.anime({
|
||||
targets : tocElement,
|
||||
duration : 200,
|
||||
easing : 'linear',
|
||||
scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top
|
||||
});
|
||||
}
|
||||
|
||||
function findIndex(entries) {
|
||||
let index = 0;
|
||||
let entry = entries[index];
|
||||
if (entry.boundingClientRect.top > 0) {
|
||||
index = sections.indexOf(entry.target);
|
||||
return index === 0 ? 0 : index - 1;
|
||||
}
|
||||
for (; index < entries.length; index++) {
|
||||
if (entries[index].boundingClientRect.top <= 0) {
|
||||
entry = entries[index];
|
||||
} else {
|
||||
return sections.indexOf(entry.target);
|
||||
}
|
||||
}
|
||||
return sections.indexOf(entry.target);
|
||||
}
|
||||
|
||||
function createIntersectionObserver(marginTop) {
|
||||
marginTop = Math.floor(marginTop + 10000);
|
||||
let intersectionObserver = new IntersectionObserver((entries, observe) => {
|
||||
let scrollHeight = document.documentElement.scrollHeight + 100;
|
||||
if (scrollHeight > marginTop) {
|
||||
observe.disconnect();
|
||||
createIntersectionObserver(scrollHeight);
|
||||
return;
|
||||
}
|
||||
let index = findIndex(entries);
|
||||
activateNavByIndex(navItems[index]);
|
||||
}, {
|
||||
rootMargin: marginTop + 'px 0px -100% 0px',
|
||||
threshold : 0
|
||||
});
|
||||
sections.forEach(element => {
|
||||
element && intersectionObserver.observe(element);
|
||||
});
|
||||
}
|
||||
createIntersectionObserver(document.documentElement.scrollHeight);
|
||||
},
|
||||
|
||||
hasMobileUA: function() {
|
||||
let ua = navigator.userAgent;
|
||||
let pa = /iPad|iPhone|Android|Opera Mini|BlackBerry|webOS|UCWEB|Blazer|PSP|IEMobile|Symbian/g;
|
||||
return pa.test(ua);
|
||||
},
|
||||
|
||||
isTablet: function() {
|
||||
return window.screen.width < 992 && window.screen.width > 767 && this.hasMobileUA();
|
||||
},
|
||||
|
||||
isMobile: function() {
|
||||
return window.screen.width < 767 && this.hasMobileUA();
|
||||
},
|
||||
|
||||
isDesktop: function() {
|
||||
return !this.isTablet() && !this.isMobile();
|
||||
},
|
||||
|
||||
supportsPDFs: function() {
|
||||
let ua = navigator.userAgent;
|
||||
let isFirefoxWithPDFJS = ua.includes('irefox') && parseInt(ua.split('rv:')[1].split('.')[0], 10) > 18;
|
||||
let supportsPdfMimeType = typeof navigator.mimeTypes['application/pdf'] !== 'undefined';
|
||||
let isIOS = /iphone|ipad|ipod/i.test(ua.toLowerCase());
|
||||
return isFirefoxWithPDFJS || (supportsPdfMimeType && !isIOS);
|
||||
},
|
||||
|
||||
/**
|
||||
* Init Sidebar & TOC inner dimensions on all pages and for all schemes.
|
||||
* Need for Sidebar/TOC inner scrolling if content taller then viewport.
|
||||
*/
|
||||
initSidebarDimension: function() {
|
||||
var sidebarNav = document.querySelector('.sidebar-nav');
|
||||
var sidebarNavHeight = sidebarNav.style.display !== 'none' ? sidebarNav.offsetHeight : 0;
|
||||
var sidebarOffset = CONFIG.sidebar.offset || 12;
|
||||
var sidebarb2tHeight = CONFIG.back2top.enable && CONFIG.back2top.sidebar ? document.querySelector('.back-to-top').offsetHeight : 0;
|
||||
var sidebarSchemePadding = (CONFIG.sidebar.padding * 2) + sidebarNavHeight + sidebarb2tHeight;
|
||||
// Margin of sidebar b2t: -4px -10px -18px, brings a different of 22px.
|
||||
if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') sidebarSchemePadding += (sidebarOffset * 2) - 22;
|
||||
// Initialize Sidebar & TOC Height.
|
||||
var sidebarWrapperHeight = document.body.offsetHeight - sidebarSchemePadding + 'px';
|
||||
document.querySelector('.site-overview-wrap').style.maxHeight = sidebarWrapperHeight;
|
||||
document.querySelector('.post-toc-wrap').style.maxHeight = sidebarWrapperHeight;
|
||||
},
|
||||
|
||||
updateSidebarPosition: function() {
|
||||
var sidebarNav = document.querySelector('.sidebar-nav');
|
||||
var hasTOC = document.querySelector('.post-toc');
|
||||
if (hasTOC) {
|
||||
sidebarNav.style.display = '';
|
||||
sidebarNav.classList.add('motion-element');
|
||||
document.querySelector('.sidebar-nav-toc').click();
|
||||
} else {
|
||||
sidebarNav.style.display = 'none';
|
||||
sidebarNav.classList.remove('motion-element');
|
||||
document.querySelector('.sidebar-nav-overview').click();
|
||||
}
|
||||
NexT.utils.initSidebarDimension();
|
||||
if (!this.isDesktop() || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return;
|
||||
// Expand sidebar on post detail page by default, when post has a toc.
|
||||
var display = CONFIG.page.sidebar;
|
||||
if (typeof display !== 'boolean') {
|
||||
// There's no definition sidebar in the page front-matter.
|
||||
display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC);
|
||||
}
|
||||
if (display) {
|
||||
window.dispatchEvent(new Event('sidebar:show'));
|
||||
}
|
||||
},
|
||||
|
||||
getScript: function(url, callback, condition) {
|
||||
if (condition) {
|
||||
callback();
|
||||
} else {
|
||||
var script = document.createElement('script');
|
||||
script.onload = script.onreadystatechange = function(_, isAbort) {
|
||||
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
|
||||
script.onload = script.onreadystatechange = null;
|
||||
script = undefined;
|
||||
if (!isAbort && callback) setTimeout(callback, 0);
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
},
|
||||
|
||||
loadComments: function(element, callback) {
|
||||
if (!CONFIG.comments.lazyload || !element) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
let intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
let entry = entries[0];
|
||||
if (entry.isIntersecting) {
|
||||
callback();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
intersectionObserver.observe(element);
|
||||
return intersectionObserver;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user