diff --git a/assets/css/_core/_widgets/_cell-tooltip.scss b/assets/css/_core/_widgets/_cell-tooltip.scss new file mode 100644 index 00000000..9940fac8 --- /dev/null +++ b/assets/css/_core/_widgets/_cell-tooltip.scss @@ -0,0 +1,11 @@ +body { + .cell-tooltip { + --tooltip-bg: #ffffff; + --tooltip-color: #0f172a; + + [data-theme=dark] & { + --tooltip-bg: #39393c; + --tooltip-color: #e2e2e6; + } + } +} diff --git a/assets/css/_core/_widgets/_index.scss b/assets/css/_core/_widgets/_index.scss index ba3172db..978d608a 100644 --- a/assets/css/_core/_widgets/_index.scss +++ b/assets/css/_core/_widgets/_index.scss @@ -1,3 +1,4 @@ +@import 'cell-tooltip'; @import 'cookieconsent'; @import 'details'; @import 'fixed-button'; diff --git a/assets/data/cdn/jsdelivr.yml b/assets/data/cdn/jsdelivr.yml index b8464bb3..7f65edf3 100644 --- a/assets/data/cdn/jsdelivr.yml +++ b/assets/data/cdn/jsdelivr.yml @@ -2,7 +2,7 @@ # See https://github.com/hugo-fixit/FixIt/issues/67 prefix: - libFiles: https://cdn.jsdelivr.net/npm/ + libFiles: https://cdn.jsdelivr.net/npm/cell-tooltip/dist/ # simple-icons@9.19.0 https://github.com/simple-icons/simple-icons simpleIcons: https://cdn.jsdelivr.net/npm/simple-icons@9.19.0/icons/ libFiles: @@ -19,6 +19,8 @@ libFiles: # autocomplete-js@0.38.1 https://github.com/algolia/autocomplete # TODO update autocompleteJS: '@algolia/autocomplete-js@1.7.1/dist/umd/index.production.js' autocompleteJS: autocomplete.js@0.38.1/dist/autocomplete.min.js + # cell-tooltip@0.1.2 https://github.com/Lruihao/cell-tooltip + cellTooltipJS: cell-tooltip@0.1.2/dist/cell-tooltip.umd.cjs # cell-watermark@1.0.3 https://github.com/Lruihao/watermark cellWatermarkJS: cell-watermark@1.0.3/src/watermark.min.js # cookieconsent@3.1.1 https://github.com/osano/cookieconsent diff --git a/assets/data/cdn/unpkg.yml b/assets/data/cdn/unpkg.yml index ac713bfe..cb2010b6 100644 --- a/assets/data/cdn/unpkg.yml +++ b/assets/data/cdn/unpkg.yml @@ -19,6 +19,8 @@ libFiles: # autocomplete-js@0.38.1 https://github.com/algolia/autocomplete # TODO update autocompleteJS: '@algolia/autocomplete-js@1.7.1/dist/umd/index.production.js' autocompleteJS: autocomplete.js@0.38.1/dist/autocomplete.min.js + # cell-tooltip@0.1.2 https://github.com/Lruihao/cell-tooltip + cellTooltipJS: cell-tooltip@0.1.2/dist/cell-tooltip.umd.cjs # cell-watermark@1.0.3 https://github.com/Lruihao/watermark cellWatermarkJS: cell-watermark@1.0.3/src/watermark.min.js # cookieconsent@3.1.1 https://github.com/osano/cookieconsent diff --git a/assets/js/theme.js b/assets/js/theme.js index 3f26dbb8..81b9743b 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -1210,6 +1210,47 @@ class FixIt { }, false); } + initFootnotes() { + const $footnoteRefs = document.querySelectorAll('#content sup[id^="fnref:"]'); + const $footnotes = document.querySelector('.footnotes[role="doc-endnotes"]'); + if (!$footnoteRefs.length || !$footnotes) return; + const footnoteMap = new Map(); + $footnoteRefs.forEach(($ref) => { + if (this.config.tooltip) { + const $link = $ref.querySelector('a.footnote-ref'); + if ($link) { + $link.addEventListener('click', (e) => { + e.preventDefault(); + }, false); + } + } + const id = $ref.id.replace('fnref:', ''); + const $footnoteContent = $footnotes.querySelector(`[id="fn:${id}"]`); + if ($footnoteContent) { + const $clonedContent = $footnoteContent.cloneNode(true); + const $backref = $clonedContent.querySelector('.footnote-backref'); + if ($backref) { + $backref.remove(); + } + footnoteMap.set($ref, $clonedContent); + } + }); + footnoteMap.forEach(($content, $ref) => { + if ($ref.hasAttribute('title')) return; + $ref.setAttribute('title', $content.textContent.trim()); + if (this.config.tooltip) { + const { Tooltip } = window.CellTooltip; + Tooltip.getOrCreateInstance($ref); + } + }); + } + + initTooltip() { + if (!this.config.tooltip) return; + const { Tooltip } = window.CellTooltip; + Tooltip.initAll('[data-ct-tooltip]'); + } + /** * Helper method to initialize content components * @param {Element} target - The target element (optional, defaults to document) @@ -1224,6 +1265,8 @@ class FixIt { this.initEcharts(); this.initTypeit(target); this.initMapbox(); + this.initFootnotes(); + this.initTooltip(); if (includeToc) { this.fixTocScroll(); this.initToc(); diff --git a/assets/lib/cell-tooltip/cell-tooltip.umd.cjs b/assets/lib/cell-tooltip/cell-tooltip.umd.cjs new file mode 100644 index 00000000..064c24e8 --- /dev/null +++ b/assets/lib/cell-tooltip/cell-tooltip.umd.cjs @@ -0,0 +1 @@ +(function(c,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(c=typeof globalThis<"u"?globalThis:c||self,f(c.CellTooltip={}))})(this,(function(c){"use strict";const f=".cell-tooltip{position:absolute;z-index:999;pointer-events:none;opacity:0;transform:scale(.97);transition:opacity .12s ease,transform .12s ease;filter:drop-shadow(0 10px 24px rgba(15,23,42,.2));--tooltip-bg: #111827;--tooltip-color: #f8fafc;--tooltip-arrow-size: 8px}.cell-tooltip[data-theme=light],.cell-tooltip[data-theme=auto]{--tooltip-bg: #ffffff;--tooltip-color: #0f172a}@media(prefers-color-scheme:dark){.cell-tooltip[data-theme=auto]{--tooltip-bg: #111827;--tooltip-color: #f8fafc}}.cell-tooltip.show{opacity:1;transform:scale(1)}.cell-tooltip-inner{max-width:240px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:1.35;color:var(--tooltip-color);background:var(--tooltip-bg);text-align:center;word-wrap:break-word}.cell-tooltip-arrow{position:absolute;width:0;height:0;transform:translate(-50%,-50%)}.cell-tooltip[data-placement=top] .cell-tooltip-arrow{bottom:calc(-1 * var(--tooltip-arrow-size));border-left:var(--tooltip-arrow-size) solid transparent;border-right:var(--tooltip-arrow-size) solid transparent;border-top:var(--tooltip-arrow-size) solid var(--tooltip-bg)}.cell-tooltip[data-placement=bottom] .cell-tooltip-arrow{top:0;border-left:var(--tooltip-arrow-size) solid transparent;border-right:var(--tooltip-arrow-size) solid transparent;border-bottom:var(--tooltip-arrow-size) solid var(--tooltip-bg)}.cell-tooltip[data-placement=left] .cell-tooltip-arrow{right:calc(-1 * var(--tooltip-arrow-size));border-top:var(--tooltip-arrow-size) solid transparent;border-bottom:var(--tooltip-arrow-size) solid transparent;border-left:var(--tooltip-arrow-size) solid var(--tooltip-bg)}.cell-tooltip[data-placement=right] .cell-tooltip-arrow{left:0;border-top:var(--tooltip-arrow-size) solid transparent;border-bottom:var(--tooltip-arrow-size) solid transparent;border-right:var(--tooltip-arrow-size) solid var(--tooltip-bg)}",d={placement:"top",trigger:"hover focus",theme:"dark",offset:8,html:!1},v={show:120,hide:80},b=["top","bottom","right","left"],w=new WeakMap;let T=0;function z(o){return T+=1,`${o}-${T}`}function y(o){return o instanceof HTMLElement}function L(o){return typeof o=="number"?{show:o,hide:o}:{show:o?.show??v.show,hide:o?.hide??v.hide}}function C(o){const t=o.trim().split(/\s+/).filter(Boolean);if(t.length===0)return["hover","focus"];const e=[];for(const i of t)(i==="hover"||i==="focus"||i==="click"||i==="manual")&&e.push(i);return e.length>0?e:["hover","focus"]}function u(o,t,e){return Math.min(Math.max(o,t),e)}function k(o){return o==="light"||o==="dark"||o==="auto"?o:d.theme}class m{element;config;tip=null;arrow=null;isEnabled=!0;timeoutId=null;hoverState=null;activeTrigger={click:!1,hover:!1,focus:!1};listeners=[];constructor(t,e={}){this.element=t;const i=t.getAttribute("data-ct-title")??t.getAttribute("title")??"",l=t.getAttribute("data-ct-placement")??void 0,r=t.getAttribute("data-ct-trigger")??void 0,s=t.getAttribute("data-ct-theme")??void 0;this.config={title:e.title??i,placement:e.placement??l??d.placement,trigger:e.trigger??r??d.trigger,theme:k(e.theme??s??d.theme),container:e.container??document.body,offset:e.offset??d.offset,html:e.html??d.html,delay:L(e.delay)},t.title&&(t.setAttribute("data-ct-original-title",t.title),t.removeAttribute("title")),this.addListeners(),w.set(t,this)}static getOrCreateInstance(t,e){return w.get(t)??new m(t,e)}static initAll(t="[data-ct-title]",e){return Array.from(document.querySelectorAll(t)).map(i=>m.getOrCreateInstance(i,e))}enable(){this.isEnabled=!0}disable(){this.isEnabled=!1}toggleEnabled(){this.isEnabled=!this.isEnabled}toggle(){if(this.tip&&this.tip.classList.contains("show")){this.leave();return}this.enter()}show(){if(!this.isEnabled||!this.hasContent())return;const t=this.getTipElement();t.dataset.theme=this.config.theme;const e=t.classList.contains("show");t.isConnected||this.config.container.appendChild(t),this.setContent(t),e||(this.updatePosition(),t.classList.add("show"),this.element.setAttribute("aria-describedby",t.id))}hide(){const t=this.tip;t&&(t.classList.remove("show"),this.activeTrigger.click=!1,this.activeTrigger.focus=!1,this.activeTrigger.hover=!1,this.hoverState=null,this.element.removeAttribute("aria-describedby"),window.setTimeout(()=>{t.classList.contains("show")||t.remove()},150))}update(){this.tip&&this.tip.classList.contains("show")&&this.updatePosition()}dispose(){this.clearTimer(),this.listeners.forEach(t=>t()),this.listeners=[],this.hide(),w.delete(this.element)}addListeners(){const t=C(this.config.trigger);for(const e of t)e!=="manual"&&(e==="hover"&&(this.bind(this.element,"mouseenter",()=>{this.activeTrigger.hover=!0,this.enter()}),this.bind(this.element,"mouseleave",()=>{this.activeTrigger.hover=!1,this.leave()})),e==="focus"&&(this.bind(this.element,"focusin",()=>{this.activeTrigger.focus=!0,this.enter()}),this.bind(this.element,"focusout",()=>{this.activeTrigger.focus=!1,this.leave()})),e==="click"&&this.bind(this.element,"click",i=>{i.preventDefault(),this.activeTrigger.click=!this.activeTrigger.click,this.toggle()}));this.bind(window,"resize",()=>this.update()),this.bind(window,"scroll",()=>{if(this.activeTrigger.hover||this.activeTrigger.focus){this.hide();return}this.update()})}bind(t,e,i){t.addEventListener(e,i),this.listeners.push(()=>t.removeEventListener(e,i))}clearTimer(){this.timeoutId!==null&&(window.clearTimeout(this.timeoutId),this.timeoutId=null)}enter(){this.isEnabled&&(this.clearTimer(),this.hoverState="show",this.timeoutId=window.setTimeout(()=>{this.hoverState==="show"&&this.show()},this.config.delay.show))}leave(){this.hasActiveTrigger()||(this.clearTimer(),this.hoverState="out",this.timeoutId=window.setTimeout(()=>{this.hoverState==="out"&&this.hide()},this.config.delay.hide))}hasActiveTrigger(){return this.activeTrigger.click||this.activeTrigger.focus||this.activeTrigger.hover}hasContent(){return this.getTitle().trim().length>0}getTitle(){const{title:t}=this.config;return typeof t=="function"?t():t}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.className="cell-tooltip",t.id=z("cell-tooltip"),t.setAttribute("role","tooltip"),t.dataset.theme=this.config.theme,t.innerHTML='
';const e=t.querySelector(".cell-tooltip-arrow");if(!y(e))throw new Error("Tooltip arrow element is missing");return this.tip=t,this.arrow=e,t}setContent(t){const e=t.querySelector(".cell-tooltip-inner");if(!y(e))throw new Error("Tooltip inner element is missing");const i=this.getTitle();if(this.config.html){e.innerHTML=i;return}e.textContent=i}updatePosition(){const t=this.getTipElement();t.style.top="0",t.style.left="0";const e=this.element.getBoundingClientRect(),i=t.getBoundingClientRect(),l=this.resolvePlacement(e,i);t.dataset.placement=l;const r=window.scrollX,s=window.scrollY,p=window.innerWidth,g=window.innerHeight,a=this.config.offset;let n=0,h=0;switch(l){case"top":n=e.top+s-i.height-a,h=e.left+r+(e.width-i.width)/2;break;case"bottom":n=e.bottom+s+a,h=e.left+r+(e.width-i.width)/2;break;case"left":n=e.top+s+(e.height-i.height)/2,h=e.left+r-i.width-1.5*a;break;case"right":n=e.top+s+(e.height-i.height)/2,h=e.right+r+a;break}const A=u(n,s+4,s+g-i.height-4),x=u(h,r+4,r+p-i.width-4);t.style.top=`${Math.round(A)}px`,t.style.left=`${Math.round(x)}px`,this.updateArrowPosition(l,e,{top:A,left:x,width:i.width,height:i.height})}resolvePlacement(t,e){if(this.config.placement!=="auto")return this.config.placement;const i=this.config.offset,l=t.top,r=window.innerHeight-t.bottom,s=t.left,p=window.innerWidth-t.right,g={top:l>=e.height+i,bottom:r>=e.height+i,left:s>=e.width+i,right:p>=e.width+i};for(const n of b)if(g[n])return n;const a={top:l,bottom:r,left:s,right:p};return b.reduce((n,h)=>a[h]>a[n]?h:n,"top")}updateArrowPosition(t,e,i){if(!this.arrow)return;const l=e.left+window.scrollX+e.width/2,r=e.top+window.scrollY+e.height/2;if(t==="top"||t==="bottom"){const g=l-i.left,a=u(g,10,i.width-10);this.arrow.style.left=`${a}px`,this.arrow.style.top="";return}const s=r-i.top,p=u(s,10,i.height-10);this.arrow.style.top=`${p}px`,this.arrow.style.left=""}}const E="cell-tooltip-runtime-style";function S(){if(typeof document>"u"||document.getElementById(E))return;const o=document.createElement("style");o.id=E,o.textContent=f,document.head.appendChild(o)}S(),c.Tooltip=m,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})})); diff --git a/hugo.toml b/hugo.toml index b64bb0a7..2af36e65 100644 --- a/hugo.toml +++ b/hugo.toml @@ -453,6 +453,8 @@ summaryPlainify = false # default is false for better performance, especially for single-language sites # set to true if your site has multiple languages and you want to show all content enableTranslationMerge = false +# FixIt 0.4.4 | NEW whether to enable tooltip replacement for elements with title attribute, such as footnote references +tooltip = true # FixIt 0.2.14 | NEW FixIt will, by default, inject a theme meta tag in the HTML head on the home page only. # You can turn it off, but we would really appreciate if you don’t, as this is a good way to watch FixIt's popularity on the rise. disableThemeInject = false diff --git a/layouts/_partials/assets.html b/layouts/_partials/assets.html index beab4010..b93bb7cd 100644 --- a/layouts/_partials/assets.html +++ b/layouts/_partials/assets.html @@ -214,6 +214,13 @@ FixIt theme assets partial {{- $config = dict "pangu" .Site.Params.pangu | merge $config -}} {{- end -}} +{{- /* Cell Tooltip */ -}} +{{- if eq .Site.Params.tooltip true -}} + {{- $source := $cdn.cellTooltipJS | default "lib/cell-tooltip/cell-tooltip.umd.cjs" -}} + {{- dict "Source" $source "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}} + {{- $config = dict "tooltip" true | merge $config -}} +{{- end -}} + {{- /* Watermark */ -}} {{- if eq .Site.Params.watermark.enable true -}} {{- $source := $cdn.cellWatermarkJS | default "lib/cell-watermark/watermark.min.js" -}} diff --git a/layouts/_partials/function/code-header.html b/layouts/_partials/function/code-header.html index 5f940bd9..c546d426 100644 --- a/layouts/_partials/function/code-header.html +++ b/layouts/_partials/function/code-header.html @@ -18,27 +18,27 @@ {{- end -}} {{- if $config.linenostoggler -}} {{- $lineNosIcon := dict "Class" "fa-solid fa-list-ol" | partial "plugin/icon.html" -}} - {{- $lineNosBtn = printf `%s` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}} + {{- $lineNosBtn = printf `%s` (T "assets.toggleLineNumbers") (T "assets.toggleLineNumbers") $lineNosIcon -}} {{- end -}} {{- if $config.linewraptoggler -}} {{- $lineWrapIcon := dict "Class" "fa-solid fa-right-left" | partial "plugin/icon.html" -}} - {{- $lineWrapBtn = printf `%s` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}} + {{- $lineWrapBtn = printf `%s` (T "assets.toggleLineWrap") (T "assets.toggleLineWrap") $lineWrapIcon -}} {{- end -}} {{- if $config.editable -}} {{- $editIcon := dict "Class" "fa-solid fa-pen-to-square" | partial "plugin/icon.html" -}} - {{- $editBtn = printf `%s` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}} + {{- $editBtn = printf `%s` (T "assets.toggleCodeEditable") (T "assets.toggleCodeEditable") $editIcon -}} {{- end -}} {{- if $config.copyable -}} {{- $copyIcon := dict "Class" "fa-regular fa-clone" | partial "plugin/icon.html" -}} - {{- $copyBtn = printf `%s` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}} + {{- $copyBtn = printf `%s` (T "assets.copyToClipboard") (T "assets.copyToClipboard") $copyIcon -}} {{- end -}} {{- if $config.downloadable -}} {{- $downloadIcon := dict "Class" "fa-solid fa-download" | partial "plugin/icon.html" -}} - {{- $downloadBtn = printf `%s` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}} + {{- $downloadBtn = printf `%s` (T "assets.downloadCode") (T "assets.downloadCode") $downloadIcon -}} {{- end -}} {{- if $config.fullscreen -}} {{- $fullscreenIcon := dict "Class" "fa-solid fa-expand" | partial "plugin/icon.html" -}} - {{- $fullscreenBtn = printf `%s` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}} + {{- $fullscreenBtn = printf `%s` (T "assets.toggleCodeFullscreen") (T "assets.toggleCodeFullscreen") $fullscreenIcon -}} {{- end -}} {{- with $config.name -}} {{- $titleEl = replace $titleEl `` (printf `` .) -}}