diff --git a/css/kunai/site.css b/css/kunai/site.css index 435263d..0c5226d 100644 --- a/css/kunai/site.css +++ b/css/kunai/site.css @@ -5,4 +5,5 @@ @import './site/sidebar.css'; @import './site/article.css'; @import './site/goog.css'; +@import './site/tooltip.css'; diff --git a/css/kunai/site/article.css b/css/kunai/site/article.css index 6745b51..281ab9c 100644 --- a/css/kunai/site/article.css +++ b/css/kunai/site/article.css @@ -238,6 +238,21 @@ div[itemtype="http://schema.org/Article"] { border: none; } } + + dfn { + font-weight: bold; + } + a.cpprefjp-defined-word { + text-decoration: underline dotted 2px #08F; + text-underline-offset: 1px; + color: inherit; + } + a.cpprefjp-defined-word:link:hover { + background-color: #DDD; + } + a.cpprefjp-defined-word[data-desc]:not(:link) { + cursor: context-menu; + } } } @@ -295,4 +310,3 @@ div[itemtype="http://schema.org/Article"] { } } } - diff --git a/css/kunai/site/tooltip.css b/css/kunai/site/tooltip.css new file mode 100644 index 0000000..4b293db --- /dev/null +++ b/css/kunai/site/tooltip.css @@ -0,0 +1,18 @@ +/* Note: The tooltip element will be directly added to the top level of */ +#kunai-ui-tooltip { + text-decoration: none!important; font-weight: normal!important; font-style: normal!important; + width: max-content; max-width: 40em; + border: 1px solid black; padding: 0.2rem 0.4rem; margin: 0; + background-color: white; color: black; text-decoration: none; font-size: 0.9rem; + position: fixed; box-shadow: 2px 2px 2px 0 rgba(128, 128, 128, 0.6); + cursor: default; + opacity: 0; z-index: -1; + transition: opacity .3s linear .5s, z-index 0s linear .8s; +} +#kunai-ui-tooltip:before { + content: attr(data-desc); +} +#kunai-ui-tooltip.kunai-ui-tooltip-revealed { + opacity: 1; z-index: 1000000; + transition: opacity .3s linear 0s, z-index 0s linear 0s; +} diff --git a/js/kunai/ui/content.js b/js/kunai/ui/content.js index 81fb428..537a6d0 100644 --- a/js/kunai/ui/content.js +++ b/js/kunai/ui/content.js @@ -1,14 +1,53 @@ import * as Badge from './badge' +import {Tooltip} from './tooltip' +const _hitElementRects = (elem, x, y) => { + for (const rect of elem.getClientRects()) + if (rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom) + return rect + return null +} + class Content { constructor(log) { this.log = log.makeContext('Content') this.log.debug('initialzing...') this.log.debug(`found ${Badge.sanitize($('main[role="main"] div[itemtype="http://schema.org/Article"] .content-body span.cpp'))} badges`) + + this.setupTooltip() + } + + setupTooltip() { + const tooltip = new Tooltip(document) + let target = null + + $('a[data-desc]').on({ + mouseover: function(e) { + const rect = _hitElementRects(this, e.clientX, e.clientY) + if (rect) { + target = this + tooltip.show(this.dataset.desc, e.clientX, e.clientY, rect) + } + }, + mouseout: function() { + if (this === target) { + target = null + tooltip.hide() + } + } + }) + + const checkScroll = function(e) { + if (target !== null && !_hitElementRects(target, e.clientX, e.clientY)) { + target = null + tooltip.hide() + } + } + window.addEventListener('scroll', checkScroll, true) + window.addEventListener('resize', checkScroll) } } export {Content} - diff --git a/js/kunai/ui/tooltip.js b/js/kunai/ui/tooltip.js new file mode 100644 index 0000000..a8fa194 --- /dev/null +++ b/js/kunai/ui/tooltip.js @@ -0,0 +1,72 @@ +class Tooltip { + /** + * ツールチップを構築します。 + * @param {Document} [_document] - 表示対象のドキュメント + * @param {object} [config] - 設定 + */ + constructor(_document, config) { + this.document = _document || document + this.view = this.document.defaultView || window + this.config = { + horizontalMargin: 8, // (設定) ツールチップ配置時のビューポート横余白 + verticalMargin: 8, // (設定) ツールチップ配置時のビューポート縦余白 + verticalOffset: 2, // (設定) ツールチップと対象要素の縦の距離 + tooltipId: 'kunai-ui-tooltip', + tooltipClassRevealed: 'kunai-ui-tooltip-revealed' + } + if (config) + Object.assign(this.config, config) + + this.span = document.createElement('span') + this.span.id = this.config.tooltipId + this.document.body.appendChild(this.span) + } + + _place(x, y) { + // 物理ピクセル位置にぴったり合わせる + const pixelRatio = this.view.devicePixelRatio + x = Math.round(x * pixelRatio) / pixelRatio + y = Math.round(y * pixelRatio) / pixelRatio + + this.span.style.left = `${x}px` + this.span.style.top = `${y}px` + this.span.classList.add(this.config.tooltipClassRevealed) + } + + /** + * マウス位置および対象領域を元にして、ツールチップを適切な位置に表示します。 + * @param {string} desc - 表示する文字列 + * @param {number} mouseX - ビューポート内のマウス位置X + * @param {number} mouseY - ビューポート内のマウス位置Y + * @param {DOMRect} rect - 表示対象オブジェクトの領域 + * + */ + show(desc, mouseX, mouseY, rect) { + // 幾何情報の取得 + this.span.dataset.desc = desc + const tw = this.span.offsetWidth // ツールチップの表示幅 + const th = this.span.offsetHeight // ツールチップの表示高さ + const vw = this.document.documentElement.clientWidth // スクロールバーを除くビューポートの幅 + const vh = this.document.documentElement.clientHeight // スクロールバーを除くビューポートの高さ + + // 位置の決定 + let x = Math.max(this.config.horizontalMargin, Math.min(vw - tw - this.config.horizontalMargin, mouseX)) + let y = rect.top - this.config.verticalOffset - th + if (y < this.config.verticalMargin) { + y = rect.bottom + this.config.verticalOffset + if (y + th > vh - this.config.verticalMargin) + y = mouseY + this.config.verticalOffset + } + + this._place(x, y) + } + + /** + * ツールチップを隠します。 + */ + hide() { + this.span.classList.remove(this.config.tooltipClassRevealed) + } +} + +export {Tooltip}