代码块添加折叠展开按钮
see https://ld246.com/article/1744373698945
简洁版
仅支持折叠/展开
缺点:由于使用mouseover事件生成按钮,有时可能折叠按钮出不来,需要鼠标移出代码块再移入即可
// 代码块添加折叠展开按钮
// 注意,由于使用mouseover事件生成按钮,有时可能折叠按钮出不来,需要鼠标移出代码块再移入即可
(()=>{
// 代码最大高度
const codeMaxHeight = '500px';
// 添加样式
addStyle(`
.b3-typography div.hljs, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.hljs {
max-height: ${codeMaxHeight};
}
.b3-typography .code-block:not(pre), .protyle-wysiwyg .code-block:not(pre){
margin: 2px 0; padding: 4px;
}
.b3-typography div.hljs, .protyle-wysiwyg div.hljs{
padding: 0.65em 1em 1.6em;
}
.b3-typography div.protyle-action, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.protyle-action {
position: sticky;
}
`);
// 绑定mouseover事件
whenElementExist('.layout__center, #editor').then((container) => {
container.addEventListener('mouseover', (event) => {
const target = event.target;
if(target.matches('.code-block')){
if(target.querySelector('.protyle-icon--expand')) return;
const code = target;
const hljs = code.querySelector('.hljs');
if(!hljs) return;
let expandStatus = getExpandStatus(hljs);
const ariaLabel = getAriaLabelText(expandStatus);
const expandBtnHtml = `<span class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--expand protyle-action__expand" aria-label="${ariaLabel}"><svg><use xlink:href="#${expandStatus}"></use></svg></span>`;
const moreBtn = code.querySelector('.protyle-icon--last');
moreBtn.insertAdjacentHTML('beforebegin', expandBtnHtml);
const expandBtn = code.querySelector('.protyle-icon--expand');
expandBtn.addEventListener('click', () => {
expandStatus = getExpandStatus(hljs);
if(expandStatus === 'iconDown') {
code.querySelector('.hljs').style.maxHeight = 'none';
expandStatus = 'iconUp';
} else {
code.querySelector('.hljs').style.maxHeight = codeMaxHeight;
expandStatus = 'iconDown';
}
const useEl = expandBtn.querySelector('svg > use');
useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#'+expandStatus);
const ariaLabel = getAriaLabelText(expandStatus);
expandBtn.setAttribute('aria-label', ariaLabel);
});
}
});
});
function getAriaLabelText(expandStatus) {
return expandStatus === 'iconDown' ? '展开' : '折叠';
}
function getExpandStatus(hljs) {
return getComputedStyle(hljs, null)?.maxHeight === codeMaxHeight ? 'iconDown' : 'iconUp';
}
function whenElementExist(selector, node) {
return new Promise(resolve => {
const check = () => {
const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
if (el) resolve(el); else requestAnimationFrame(check);
};
check();
});
}
function addStyle(css) {
// 创建一个新的style元素
const style = document.createElement('style');
// 设置CSS规则
style.innerHTML = css;
// 将style元素添加到<head>中
document.head.appendChild(style);
}
})();
完善版
仅支持折叠/展开
// 代码块添加折叠展开按钮
(()=>{
// 当代码块内容最大高度,注意:这里的高度是指.hljs元素的高度,默认是500px
const codeMaxHeight = '500px';
// 添加样式
addStyle(`
.b3-typography div.hljs, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.hljs {
max-height: ${codeMaxHeight||'500px'};
}
.b3-typography .code-block:not(pre), .protyle-wysiwyg .code-block:not(pre){
margin: 2px 0; padding: 4px;
}
.b3-typography div.hljs, .protyle-wysiwyg div.hljs{
padding: 0.65em 1em 1.6em;
}
.b3-typography div.protyle-action, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.protyle-action {
position: sticky;
}
`);
// 监听代码块被加载
whenElementExist('.layout__center, #editor').then(async el => {
// 加载时执行(静态加载)
let protyle;
await whenElementExist(() => {
protyle = el.querySelector('.protyle');
return protyle && protyle?.dataset?.loading === 'finished';
});
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'));
//滚动时执行
protyle.querySelector(".protyle-content").addEventListener('scroll', () => {
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'));
});
// 监听protyle加载事件(动态加载)
observeProtyleLoaded(el, protyles => {
protyles.forEach(async protyle => {
if(!protyle.classList.contains('protyle')) {
protyle = protyle.closest('.protyle');
}
// 加载时执行
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'));
// 滚动时执行
protyle.querySelector(".protyle-content").addEventListener('scroll', () => {
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'));
});
});
});
});
// 添加扩展按钮
let runing = false;
function addCodeExtends(codeBlocks) {
if(codeBlocks.length === 0) return;
if(runing) return;
runing = true;
setTimeout(() => {runing = false;}, 300);
codeBlocks.forEach(async code => {
if(code.querySelector('.protyle-icon--expand')) return;
const hljs = code.querySelector('.hljs');
if(!hljs) return;
let expandStatus = getExpandStatus(hljs);
const ariaLabel = getAriaLabelText(expandStatus);
const expandBtnHtml = `<span class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--expand protyle-action__expand protyle-custom" aria-label="${ariaLabel}"><svg><use xlink:href="#${expandStatus}"></use></svg></span>`;
const moreBtn = code.querySelector('.protyle-icon--last');
if(!moreBtn) return;
await whenElementExist(()=>moreBtn.getAttribute('aria-label'));
moreBtn.insertAdjacentHTML('beforebegin', expandBtnHtml);
const expandBtn = code.querySelector('.protyle-icon--expand');
expandBtn.addEventListener('click', () => {
expandStatus = getExpandStatus(hljs);
if(expandStatus === 'iconDown') {
code.querySelector('.hljs').style.maxHeight = 'none';
expandStatus = 'iconUp';
} else {
code.querySelector('.hljs').style.maxHeight = codeMaxHeight;
expandStatus = 'iconDown';
}
const useEl = expandBtn.querySelector('svg > use');
useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#'+expandStatus);
const ariaLabel = getAriaLabelText(expandStatus);
expandBtn.setAttribute('aria-label', ariaLabel);
});
});
}
function getAriaLabelText(expandStatus) {
return expandStatus === 'iconDown' ? '展开' : '折叠';
}
function getExpandStatus(hljs) {
return getComputedStyle(hljs, null)?.maxHeight === codeMaxHeight ? 'iconDown' : 'iconUp';
}
function addStyle(css) {
// 创建一个新的style元素
const style = document.createElement('style');
// 设置CSS规则
style.innerHTML = css;
// 将style元素添加到<head>中
document.head.appendChild(style);
}
function observeProtyleLoaded(el, callback) {
const config = { attributes: false, childList: true, subtree: true };
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => {
if (mutation.type === 'childList') {
// 查找新增加的 NodeCodeBlock 元素
const protyles = Array.from(mutation.addedNodes).filter(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('protyle') || node.classList.contains('protyle-content'))
);
// 如果找到了这样的元素,则调用回调函数
if (protyles.length > 0) {
callback(protyles);
}
}
});
});
// 开始观察 body 下的所有变化
observer.observe(el, config);
// 当不再需要观察时可调用断开来停止观察
return () => {
observer.disconnect();
};
}
// 等待元素渲染完成后执行
function whenElementExist(selector, node) {
return new Promise(resolve => {
const check = () => {
const el = typeof selector==='function'?selector():(node||document).querySelector(selector);
if (el) resolve(el); else requestAnimationFrame(check);
};
check();
});
}
})();
折叠,全屏,悬浮滚动条版
// 代码块添加折叠/展开/全屏/悬浮横向滚动条
// see https://ld246.com/article/1744373698945
// version 0.0.1
// 0.0.1 支持代码块的折叠和展开,全屏和悬浮横向滚动条
(() => {
// 当代码块内容最大高度,注意:这里的高度是指.hljs元素的高度,默认是500px
const codeMaxHeight = '500px';
// 是否显示全屏按钮 true 显示 false 不显示
const isEnableFullscreen = true;
// 是否显示模拟滚动条 true 显示 false 不显示
// 该功能在代码块底部超出可视区域时自动在底部显示滚动条
const isEnableScrollbar = true;
// 不支持手机版(因为手机版不需要)
if(isMobile()) return;
// 添加样式
addStyle(`
.b3-typography div.hljs, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.hljs {
max-height: ${codeMaxHeight || '500px'};
}
.b3-typography .code-block:not(pre), .protyle-wysiwyg .code-block:not(pre){
margin: 2px 0; padding: 4px;
}
.b3-typography div.hljs, .protyle-wysiwyg div.hljs{
padding: 0.65em 1em 1.6em;
}
.b3-typography div.protyle-action, .protyle-wysiwyg .code-block:not([custom-auto-height]) div.protyle-action {
position: sticky;
}
/* 全屏背景色 */
:not(:root):fullscreen::backdrop {
background-color: var(--b3-theme-background);
}
/* 模拟滚动条容器 */
.scrollbar-container {
position: sticky;
bottom: 0;
width: 100%;
height: 10px;
background-color: #ddd;
cursor: pointer;
border-radius: 5px;
/*transition: opacity 0.3s ease;*/
}
/* 滚动条滑块 */
.scrollbar-thumb {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 20%; /* 初始滑块宽度 */
background-color: #666;
border-radius: 5px;
cursor: grab;
}
.scrollbar-container.f__hidden {
opacity: 0; /* 隐藏元素 */
pointer-events: none; /* 禁用鼠标交互 */
height: 0;
}
`);
// 监听代码块被加载
whenElementExist('.layout__center, #editor').then(async el => {
// 加载时执行(静态加载)
let protyle;
await whenElementExist(() => {
protyle = el.querySelector('.protyle');
return protyle && protyle?.dataset?.loading === 'finished';
});
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'), protyle);
// 滚动时执行
protyle.querySelector(".protyle-content").addEventListener('scroll', () => {
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'), protyle);
});
// 监听protyle加载事件(动态加载)
observeProtyleLoaded(el, protyles => {
protyles.forEach(async protyle => {
if (!protyle.classList.contains('protyle')) {
protyle = protyle.closest('.protyle');
}
// 加载时执行
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'), protyle);
// 滚动时执行
protyle.querySelector(".protyle-content").addEventListener('scroll', () => {
addCodeExtends(protyle.querySelectorAll('.code-block:not(:has(.protyle-icon--expand))'), protyle);
});
});
});
});
// 添加扩展按钮
let runing = false;
function addCodeExtends(codeBlocks, protyle) {
if (codeBlocks.length === 0) return;
if (runing) return;
runing = true;
setTimeout(() => { runing = false; }, 300);
codeBlocks.forEach(async code => {
if (code.querySelector('.protyle-icon--expand')) return;
// 添加折叠按钮
const hljs = code.querySelector('.hljs');
if (!hljs) return;
let expandStatus = getExpandStatus(hljs);
const ariaLabel = getAriaLabelText(expandStatus);
const expandBtnHtml = `<span class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--expand protyle-action__expand protyle-custom" aria-label="${ariaLabel}"><svg><use xlink:href="#${expandStatus}"></use></svg></span>`;
const moreBtn = code.querySelector('.protyle-icon--last');
if (!moreBtn) return;
await whenElementExist(() => moreBtn.getAttribute('aria-label'));
moreBtn.insertAdjacentHTML('beforebegin', expandBtnHtml);
const expandBtn = code.querySelector('.protyle-icon--expand');
expandBtn.addEventListener('click', () => {
expandStatus = getExpandStatus(hljs);
if (expandStatus === 'iconDown') {
hljs.style.maxHeight = 'none';
expandStatus = 'iconUp';
} else {
hljs.style.maxHeight = codeMaxHeight;
expandStatus = 'iconDown';
}
const useEl = expandBtn.querySelector('svg > use');
useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + expandStatus);
const ariaLabel = getAriaLabelText(expandStatus);
expandBtn.setAttribute('aria-label', ariaLabel);
});
// 添加全屏按钮
if (!isEnableFullscreen) return;
if (code.querySelector('.protyle-icon--fullscreen')) return;
let fullscreenAriaLabel = '全屏';
let fullscreenStatus = 'iconFullscreen';
const fullscreenBtnHtml = `<span class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--fullscreen protyle-action__fullscreen protyle-custom" aria-label="${fullscreenAriaLabel}"><svg><use xlink:href="#${fullscreenStatus}"></use></svg></span>`;
expandBtn.insertAdjacentHTML('beforebegin', fullscreenBtnHtml);
const fullscreenBtn = code.querySelector('.protyle-icon--fullscreen');
let oldCodeMaxHeight;
fullscreenBtn.addEventListener('click', () => {
if (fullscreenStatus === 'iconFullscreen') {
oldCodeMaxHeight = hljs.style.maxHeight;
requestFullScreen(code);
fullscreenStatus = 'iconFullscreenExit';
fullscreenAriaLabel = '退出全屏';
hljs.style.maxHeight = '100vh';
expandBtn.style.display = 'none';
} else {
exitFullScreen(code);
fullscreenStatus = 'iconFullscreen';
fullscreenAriaLabel = '全屏';
if (oldCodeMaxHeight !== undefined) hljs.style.maxHeight = oldCodeMaxHeight;
expandBtn.style.display = '';
}
const useEl = fullscreenBtn.querySelector('svg > use');
useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + fullscreenStatus);
fullscreenBtn.setAttribute('aria-label', fullscreenAriaLabel);
});
// 添加模拟滚动条
if (!isEnableScrollbar) return;
if (code.querySelector('.scrollbar-container')) return;
const scrollbarHtml = `<div class="scrollbar-container protyle-custom"><div class="scrollbar-thumb"></div></div>`;
code.insertAdjacentHTML('beforeend', scrollbarHtml);
const scrollbarContainer = code.querySelector('.scrollbar-container');
const protyleContent = protyle.querySelector(".protyle-content");
protyleContent.addEventListener('scroll', () => {
// 判断是否仍然处于 sticky 状态
if (isElementBottomInViewport(code)) {
// 如果元素底部超出了视口高度,说明已离开 sticky 状态
scrollbarContainer.classList.add('f__hidden');
} else {
// 否则保持可见
scrollbarContainer.classList.remove('f__hidden');
}
});
// 模拟滚动条滚动
const scrollbarThumb = code.querySelector(".scrollbar-thumb");
let isDragging = false; // 是否正在拖动
let startX, thumbStartX; // 鼠标按下时的初始位置
// 计算滑块宽度和滚动比例
function updateScrollbar() {
const contentWidth = hljs.scrollWidth;
const viewportWidth = hljs.clientWidth;
let thumbWidth = (viewportWidth / contentWidth) * scrollbarContainer.offsetWidth;
// 边界值处理
thumbWidth = Math.max(thumbWidth, 10); // 最小宽度为10px
scrollbarThumb.style.width = `${thumbWidth}px`;
}
// 同步滚动条位置
function syncScrollbarPosition() {
const scrollPercentage =
hljs.scrollLeft / (hljs.scrollWidth - hljs.clientWidth);
const thumbMaxMove = scrollbarContainer.offsetWidth - scrollbarThumb.offsetWidth;
scrollbarThumb.style.left = `${scrollPercentage * thumbMaxMove}px`;
}
// 初始化滚动条
updateScrollbar();
syncScrollbarPosition();
// 监听 .code-block 的滚动事件
hljs.addEventListener("scroll", () => {
syncScrollbarPosition();
});
// 模拟滚动条拖动逻辑
scrollbarThumb.addEventListener("mousedown", (e) => {
isDragging = true;
startX = e.clientX;
thumbStartX = parseFloat(scrollbarThumb.style.left) || 0;
// 禁用文本选择
hljs.style.userSelect = "none";
// 绑定全局事件
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
// 阻止默认行为
e.preventDefault();
});
function handleMouseMove(e) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const thumbMaxMove = scrollbarContainer.offsetWidth - scrollbarThumb.offsetWidth;
let newThumbPosition = thumbStartX + deltaX;
// 限制滑块范围
newThumbPosition = Math.max(0, Math.min(newThumbPosition, thumbMaxMove));
scrollbarThumb.style.left = `${newThumbPosition}px`;
// 同步 .code-block 的滚动位置
const scrollPercentage = newThumbPosition / thumbMaxMove;
hljs.scrollLeft = scrollPercentage * (hljs.scrollWidth - hljs.clientWidth);
// 阻止默认行为
e.preventDefault();
}
function handleMouseUp() {
isDragging = false;
// 恢复文本选择
hljs.style.userSelect = "";
// 移除全局事件
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
// 监听窗口大小变化
window.addEventListener("resize", () => {
updateScrollbar();
syncScrollbarPosition();
});
});
}
function requestFullScreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { // Firefox
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { // Chrome, Safari, Opera
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { // IE/Edge
element.msRequestFullscreen();
}
}
function exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { // Firefox
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { // Chrome, Safari, Opera
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE/Edge
document.msExitFullscreen();
}
}
function getAriaLabelText(expandStatus) {
return expandStatus === 'iconDown' ? '展开' : '折叠';
}
function getExpandStatus(hljs) {
return getComputedStyle(hljs, null)?.maxHeight === codeMaxHeight ? 'iconDown' : 'iconUp';
}
function addStyle(css) {
// 创建一个新的style元素
const style = document.createElement('style');
// 设置CSS规则
style.innerHTML = css;
// 将style元素添加到<head>中
document.head.appendChild(style);
}
function observeProtyleLoaded(el, callback) {
const config = { attributes: false, childList: true, subtree: true };
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => {
if (mutation.type === 'childList') {
// 查找新增加的 NodeCodeBlock 元素
const protyles = Array.from(mutation.addedNodes).filter(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('protyle') || node.classList.contains('protyle-content'))
);
// 如果找到了这样的元素,则调用回调函数
if (protyles.length > 0) {
callback(protyles);
}
}
});
});
// 开始观察 body 下的所有变化
observer.observe(el, config);
// 当不再需要观察时可调用断开来停止观察
return () => {
observer.disconnect();
};
}
// 等待元素渲染完成后执行
function whenElementExist(selector, node) {
return new Promise(resolve => {
const check = () => {
const el = typeof selector === 'function' ? selector() : (node || document).querySelector(selector);
if (el) resolve(el); else requestAnimationFrame(check);
};
check();
});
}
function isMobile() {
return !!document.getElementById("sidebar");
}
function isElementBottomInViewport(el) {
if (!el) return false; // 如果元素不存在,直接返回 false
const rect = el.getBoundingClientRect(); // 获取元素的边界信息
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
// 判断元素的底部是否在视口内
return rect.bottom <= windowHeight;
}
let statusMsg;
function showStatusMsg(params, append = false) {
if (!statusMsg) statusMsg = document.querySelector('#status .status__msg');
params = typeof params === 'string' ? params : JSON.stringify(params);
let html = statusMsg.innerHTML;
if (append) {
html += params;
} else {
html = params;
}
html = html.trim();
statusMsg.innerHTML = html;
}
})();
