Merge branch 'canary' into master (#711)
* refactor(i18n): simplify the structure of the translation file * chore: prepare repository for v4.0.0-alpha * chore: modify go.mod to v4 * refactor: migrate theme configuration to TOML * fix: exampleSite not using correct theme * feat: add favicon from assets folder * refactor: drop linear grandient background feature remove node-vibrant from dependencies * feat: use Hugo's code block render hook to implement code copy button Now it can have i18n support * refactor: delete color.ts * refactor: delete Emoji support post from example site * refactor: drop support for `hidden` field in front matter * feat: upgrade to PhotoSwipe v5 * chore: bump the required hugo version to 0.100.0 * refactor: remove PhotoSwipe from external.yaml * fix: extra margin in search result * fix: incorrect markdown heading level in example site * refactor: remove some usages of `default` in template No longer needed thanks to Hugo's configuration merge * fix: one line codeblock style in firefox closes https://github.com/CaiJimmy/hugo-theme-stack/issues/564 * feat: add style to new codeblock * feat: i18n support for codeblock copy text * fix(menu): jitter when closing menu It's caused by flexbox gap property, which can't animate * fix(search): long text overflows under the Search icon closes https://github.com/CaiJimmy/hugo-theme-stack/issues/515
This commit is contained in:
@@ -192,7 +192,6 @@
|
||||
.article-preview {
|
||||
font-size: 1.4rem;
|
||||
color: var(--card-text-color-tertiary);
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ $text-color: $color;
|
||||
$name-color: #a6e22e;
|
||||
$literal-color: #e6db74;
|
||||
|
||||
@import "common.scss";
|
||||
@import "common.scss";
|
||||
@@ -286,10 +286,12 @@
|
||||
line-height: 1.428571429;
|
||||
word-break: break-all;
|
||||
padding: var(--card-padding);
|
||||
|
||||
// keep Codeblocks LTR
|
||||
[dir="rtl"] & {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
code {
|
||||
color: unset;
|
||||
border: none;
|
||||
@@ -303,15 +305,11 @@
|
||||
padding: var(--card-padding);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copyCodeButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
// keep Codeblocks LTR
|
||||
[dir="rtl"] & {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: initial;
|
||||
padding: 0;
|
||||
@@ -320,20 +318,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.copyCodeButton {
|
||||
position: absolute;
|
||||
top: calc(var(--card-padding));
|
||||
right: calc(var(--card-padding));
|
||||
background: var(--card-background);
|
||||
border: none;
|
||||
box-shadow: var(--shadow-l2);
|
||||
border-radius: var(--tag-border-radius);
|
||||
padding: 8px 16px;
|
||||
color: var(--card-text-color-main);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
.codeblock {
|
||||
header {
|
||||
background-color: var(--card-background-selected);
|
||||
padding: 5px var(--card-padding);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow-l1);
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: var(--card-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.codeblock-copy {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
color: var(--card-text-color-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
@@ -402,7 +410,7 @@
|
||||
/// Negative margins
|
||||
blockquote,
|
||||
figure,
|
||||
.highlight,
|
||||
.codeblock,
|
||||
pre,
|
||||
.gallery,
|
||||
.video-wrapper,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
input {
|
||||
padding: 40px 20px 20px;
|
||||
padding-inline-end: var(--button-size);
|
||||
border-radius: var(--card-border-radius);
|
||||
background-color: var(--card-background);
|
||||
box-shadow: var(--shadow-l1);
|
||||
@@ -78,5 +79,4 @@
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
gap: var(--sidebar-element-separation);
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -65,6 +64,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.social-menu,
|
||||
.menu {
|
||||
margin-top: var(--sidebar-element-separation);
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copy button for code blocks
|
||||
*/
|
||||
export default () => {
|
||||
const copyButtons = document.querySelectorAll('.codeblock-copy');
|
||||
copyButtons.forEach(button => {
|
||||
const codeblockID = button.getAttribute('data-id'),
|
||||
copyText = button.textContent,
|
||||
copiedText = button.getAttribute('data-copied-text');
|
||||
if (!codeblockID) return;
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const codeblock = document.getElementById(codeblockID) as HTMLElement;
|
||||
if (!codeblockID) return;
|
||||
navigator.clipboard.writeText(codeblock.textContent)
|
||||
.then(() => {
|
||||
button.textContent = copiedText;
|
||||
setTimeout(() => {
|
||||
button.textContent = copyText;
|
||||
}, 1000);
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err)
|
||||
console.log('Something went wrong', err);
|
||||
});
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
interface colorScheme {
|
||||
hash: string, /// Regenerate color scheme when the image hash is changed
|
||||
DarkMuted: {
|
||||
hex: string,
|
||||
rgb: Number[],
|
||||
bodyTextColor: string
|
||||
},
|
||||
Vibrant: {
|
||||
hex: string,
|
||||
rgb: Number[],
|
||||
bodyTextColor: string
|
||||
}
|
||||
}
|
||||
|
||||
let colorsCache: { [key: string]: colorScheme } = {};
|
||||
|
||||
if (localStorage.hasOwnProperty('StackColorsCache')) {
|
||||
try {
|
||||
colorsCache = JSON.parse(localStorage.getItem('StackColorsCache'));
|
||||
}
|
||||
catch (e) {
|
||||
colorsCache = {};
|
||||
}
|
||||
}
|
||||
|
||||
async function getColor(key: string, hash: string, imageURL: string) {
|
||||
if (!key) {
|
||||
/**
|
||||
* If no key is provided, do not cache the result
|
||||
*/
|
||||
return await Vibrant.from(imageURL).getPalette();
|
||||
}
|
||||
|
||||
if (!colorsCache.hasOwnProperty(key) || colorsCache[key].hash !== hash) {
|
||||
/**
|
||||
* If key is provided, but not found in cache, or the hash mismatches => Regenerate color scheme
|
||||
*/
|
||||
const palette = await Vibrant.from(imageURL).getPalette();
|
||||
|
||||
colorsCache[key] = {
|
||||
hash: hash,
|
||||
Vibrant: {
|
||||
hex: palette.Vibrant.hex,
|
||||
rgb: palette.Vibrant.rgb,
|
||||
bodyTextColor: palette.Vibrant.bodyTextColor
|
||||
},
|
||||
DarkMuted: {
|
||||
hex: palette.DarkMuted.hex,
|
||||
rgb: palette.DarkMuted.rgb,
|
||||
bodyTextColor: palette.DarkMuted.bodyTextColor
|
||||
}
|
||||
}
|
||||
|
||||
/* Save the result in localStorage */
|
||||
localStorage.setItem('StackColorsCache', JSON.stringify(colorsCache));
|
||||
}
|
||||
|
||||
return colorsCache[key];
|
||||
}
|
||||
|
||||
export {
|
||||
getColor
|
||||
}
|
||||
+71
-165
@@ -1,186 +1,92 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
PhotoSwipe: any;
|
||||
PhotoSwipeUI_Default: any
|
||||
const wrap = (figures: HTMLElement[]) => {
|
||||
const galleryContainer = document.createElement('div');
|
||||
galleryContainer.className = 'gallery';
|
||||
|
||||
const parentNode = figures[0].parentNode,
|
||||
first = figures[0];
|
||||
|
||||
parentNode.insertBefore(galleryContainer, first)
|
||||
|
||||
for (const figure of figures) {
|
||||
galleryContainer.appendChild(figure);
|
||||
}
|
||||
}
|
||||
|
||||
interface PhotoSwipeItem {
|
||||
w: number;
|
||||
h: number;
|
||||
src: string;
|
||||
msrc: string;
|
||||
title?: string;
|
||||
el: HTMLElement;
|
||||
}
|
||||
export default (container: HTMLElement) => {
|
||||
/// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
|
||||
/// because it can not detect whether image is being wrapped by a link or not
|
||||
/// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
|
||||
const images = container.querySelectorAll('img.gallery-image') as NodeListOf<HTMLImageElement>;
|
||||
for (const img of Array.from(images)) {
|
||||
/// Images are wrapped with figure tag if the paragraph has only images without texts
|
||||
/// This is done to allow inline images within paragraphs
|
||||
const paragraph = img.closest('p');
|
||||
|
||||
class StackGallery {
|
||||
private galleryUID: number;
|
||||
private items: PhotoSwipeItem[] = [];
|
||||
if (!paragraph || !container.contains(paragraph)) continue;
|
||||
|
||||
constructor(container: HTMLElement, galleryUID = 1) {
|
||||
if (window.PhotoSwipe == undefined || window.PhotoSwipeUI_Default == undefined) {
|
||||
console.error("PhotoSwipe lib not loaded.");
|
||||
return;
|
||||
if (paragraph.textContent.trim() == '') {
|
||||
/// Once we insert figcaption, this check no longer works
|
||||
/// So we add a class to paragraph to mark it
|
||||
paragraph.classList.add('no-text');
|
||||
}
|
||||
|
||||
this.galleryUID = galleryUID;
|
||||
let isNewLineImage = paragraph.classList.contains('no-text');
|
||||
if (!isNewLineImage) continue;
|
||||
|
||||
StackGallery.createGallery(container);
|
||||
this.loadItems(container);
|
||||
this.bindClick();
|
||||
}
|
||||
const hasLink = img.parentElement.tagName == 'A';
|
||||
|
||||
private loadItems(container: HTMLElement) {
|
||||
this.items = [];
|
||||
let el: HTMLElement = img;
|
||||
/// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
|
||||
const figure = document.createElement('figure');
|
||||
figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
|
||||
figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
|
||||
if (hasLink) {
|
||||
/// Wrap <a> if it exists
|
||||
el = img.parentElement;
|
||||
}
|
||||
el.parentElement.insertBefore(figure, el);
|
||||
figure.appendChild(el);
|
||||
|
||||
const figures = container.querySelectorAll('figure.gallery-image');
|
||||
/// Add figcaption if it exists
|
||||
if (img.hasAttribute('alt')) {
|
||||
const figcaption = document.createElement('figcaption');
|
||||
figcaption.innerText = img.getAttribute('alt');
|
||||
figure.appendChild(figcaption);
|
||||
}
|
||||
|
||||
for (const el of figures) {
|
||||
const figcaption = el.querySelector('figcaption'),
|
||||
img = el.querySelector('img');
|
||||
/// Wrap img tag with <a> tag if image was not wrapped by <a> tag
|
||||
if (!hasLink) {
|
||||
figure.className = 'gallery-image';
|
||||
|
||||
let aux: PhotoSwipeItem = {
|
||||
w: parseInt(img.getAttribute('width')),
|
||||
h: parseInt(img.getAttribute('height')),
|
||||
src: img.src,
|
||||
msrc: img.getAttribute('data-thumb') || img.src,
|
||||
el: el
|
||||
}
|
||||
|
||||
if (figcaption) {
|
||||
aux.title = figcaption.innerHTML;
|
||||
}
|
||||
|
||||
this.items.push(aux);
|
||||
const a = document.createElement('a');
|
||||
a.href = img.src;
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('data-pswp-width', img.width.toString());
|
||||
a.setAttribute('data-pswp-height', img.height.toString());
|
||||
img.parentNode.insertBefore(a, img);
|
||||
a.appendChild(img);
|
||||
}
|
||||
}
|
||||
|
||||
public static createGallery(container: HTMLElement) {
|
||||
/// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
|
||||
/// because it can not detect whether image is being wrapped by a link or not
|
||||
/// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
|
||||
|
||||
const images = container.querySelectorAll('img.gallery-image');
|
||||
for (const img of Array.from(images)) {
|
||||
/// Images are wrapped with figure tag if the paragraph has only images without texts
|
||||
/// This is done to allow inline images within paragraphs
|
||||
const paragraph = img.closest('p');
|
||||
|
||||
if (!paragraph || !container.contains(paragraph)) continue;
|
||||
|
||||
if (paragraph.textContent.trim() == '') {
|
||||
/// Once we insert figcaption, this check no longer works
|
||||
/// So we add a class to paragraph to mark it
|
||||
paragraph.classList.add('no-text');
|
||||
}
|
||||
|
||||
let isNewLineImage = paragraph.classList.contains('no-text');
|
||||
if (!isNewLineImage) continue;
|
||||
|
||||
const hasLink = img.parentElement.tagName == 'A';
|
||||
|
||||
let el: HTMLElement = img;
|
||||
/// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
|
||||
const figure = document.createElement('figure');
|
||||
figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
|
||||
figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
|
||||
if (hasLink) {
|
||||
/// Wrap <a> if it exists
|
||||
el = img.parentElement;
|
||||
}
|
||||
el.parentElement.insertBefore(figure, el);
|
||||
figure.appendChild(el);
|
||||
|
||||
/// Add figcaption if it exists
|
||||
if (img.hasAttribute('alt')) {
|
||||
const figcaption = document.createElement('figcaption');
|
||||
figcaption.innerText = img.getAttribute('alt');
|
||||
figure.appendChild(figcaption);
|
||||
}
|
||||
|
||||
/// Wrap img tag with <a> tag if image was not wrapped by <a> tag
|
||||
if (!hasLink) {
|
||||
figure.className = 'gallery-image';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = img.src;
|
||||
a.setAttribute('target', '_blank');
|
||||
img.parentNode.insertBefore(a, img);
|
||||
a.appendChild(img);
|
||||
}
|
||||
const figuresEl = container.querySelectorAll('figure.gallery-image') as NodeListOf<HTMLElement>;
|
||||
let currentGallery = [];
|
||||
for (const figure of Array.from(figuresEl)) {
|
||||
if (!currentGallery.length) {
|
||||
/// First iteration
|
||||
currentGallery = [figure];
|
||||
}
|
||||
|
||||
const figuresEl = container.querySelectorAll('figure.gallery-image');
|
||||
|
||||
let currentGallery = [];
|
||||
|
||||
for (const figure of figuresEl) {
|
||||
if (!currentGallery.length) {
|
||||
/// First iteration
|
||||
currentGallery = [figure];
|
||||
}
|
||||
else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) {
|
||||
/// Adjacent figures
|
||||
currentGallery.push(figure);
|
||||
}
|
||||
else if (currentGallery.length) {
|
||||
/// End gallery
|
||||
StackGallery.wrap(currentGallery);
|
||||
currentGallery = [figure];
|
||||
}
|
||||
else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) {
|
||||
/// Adjacent figures
|
||||
currentGallery.push(figure);
|
||||
}
|
||||
|
||||
if (currentGallery.length > 0) {
|
||||
StackGallery.wrap(currentGallery);
|
||||
else if (currentGallery.length) {
|
||||
/// End gallery
|
||||
wrap(currentGallery);
|
||||
currentGallery = [figure];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap adjacent figure tags with div.gallery
|
||||
* @param figures
|
||||
*/
|
||||
public static wrap(figures: HTMLElement[]) {
|
||||
const galleryContainer = document.createElement('div');
|
||||
galleryContainer.className = 'gallery';
|
||||
|
||||
const parentNode = figures[0].parentNode,
|
||||
first = figures[0];
|
||||
|
||||
parentNode.insertBefore(galleryContainer, first)
|
||||
|
||||
for (const figure of figures) {
|
||||
galleryContainer.appendChild(figure);
|
||||
}
|
||||
if (currentGallery.length > 0) {
|
||||
wrap(currentGallery);
|
||||
}
|
||||
|
||||
public open(index: number) {
|
||||
const pswp = document.querySelector('.pswp') as HTMLDivElement;
|
||||
const ps = new window.PhotoSwipe(pswp, window.PhotoSwipeUI_Default, this.items, {
|
||||
index: index,
|
||||
galleryUID: this.galleryUID,
|
||||
getThumbBoundsFn: (index) => {
|
||||
const thumbnail = this.items[index].el.getElementsByTagName('img')[0],
|
||||
pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
|
||||
rect = thumbnail.getBoundingClientRect();
|
||||
|
||||
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
|
||||
}
|
||||
});
|
||||
|
||||
ps.init();
|
||||
}
|
||||
|
||||
private bindClick() {
|
||||
for (const [index, item] of this.items.entries()) {
|
||||
const a = item.el.querySelector('a');
|
||||
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.open(index);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StackGallery;
|
||||
};
|
||||
+2
-67
@@ -5,8 +5,7 @@
|
||||
* @website: https://jimmycai.com
|
||||
* @link: https://github.com/CaiJimmy/hugo-theme-stack
|
||||
*/
|
||||
import StackGallery from "ts/gallery";
|
||||
import { getColor } from 'ts/color';
|
||||
import StackCodeBlock from "ts/codeblock";
|
||||
import menu from 'ts/menu';
|
||||
import createElement from 'ts/createElement';
|
||||
import StackColorScheme from 'ts/colorScheme';
|
||||
@@ -22,76 +21,12 @@ let Stack = {
|
||||
|
||||
const articleContent = document.querySelector('.article-content') as HTMLElement;
|
||||
if (articleContent) {
|
||||
new StackGallery(articleContent);
|
||||
setupSmoothAnchors();
|
||||
setupScrollspy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add linear gradient background to tile style article
|
||||
*/
|
||||
const articleTile = document.querySelector('.article-list--tile');
|
||||
if (articleTile) {
|
||||
let observer = new IntersectionObserver(async (entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(entry.target);
|
||||
|
||||
const articles = entry.target.querySelectorAll('article.has-image');
|
||||
articles.forEach(async articles => {
|
||||
const image = articles.querySelector('img'),
|
||||
imageURL = image.src,
|
||||
key = image.getAttribute('data-key'),
|
||||
hash = image.getAttribute('data-hash'),
|
||||
articleDetails: HTMLDivElement = articles.querySelector('.article-details');
|
||||
|
||||
const colors = await getColor(key, hash, imageURL);
|
||||
|
||||
articleDetails.style.background = `
|
||||
linear-gradient(0deg,
|
||||
rgba(${colors.DarkMuted.rgb[0]}, ${colors.DarkMuted.rgb[1]}, ${colors.DarkMuted.rgb[2]}, 0.5) 0%,
|
||||
rgba(${colors.Vibrant.rgb[0]}, ${colors.Vibrant.rgb[1]}, ${colors.Vibrant.rgb[2]}, 0.75) 100%)`;
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
observer.observe(articleTile)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add copy button to code block
|
||||
*/
|
||||
const highlights = document.querySelectorAll('.article-content div.highlight');
|
||||
const copyText = `Copy`,
|
||||
copiedText = `Copied!`;
|
||||
|
||||
highlights.forEach(highlight => {
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.innerHTML = copyText;
|
||||
copyButton.classList.add('copyCodeButton');
|
||||
highlight.appendChild(copyButton);
|
||||
|
||||
const codeBlock = highlight.querySelector('code[data-lang]');
|
||||
if (!codeBlock) return;
|
||||
|
||||
copyButton.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(codeBlock.textContent)
|
||||
.then(() => {
|
||||
copyButton.textContent = copiedText;
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = copyText;
|
||||
}, 1000);
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err)
|
||||
console.log('Something went wrong', err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
new StackColorScheme(document.getElementById('dark-mode-toggle'));
|
||||
StackCodeBlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user