如果用户代理工具提示无法解决问题,则必须自己实现一些功能。我决定仍然依赖声明性的 desc 元素,即使我们自己使用渲染工具提示,我们也可以轻松使用这些元素。
在下面的 SVG 文档中,工具提示定义用作模板,每当“鼠标”指针(任何可以生成“鼠标*”事件的东西,真的)进入一个元素时,我们都会提取文档片段(@987654322 @) 即其desc 元素的内容,并将这些内容复制到工具提示的“内容”组/图形中。最重要的是,我们计算工具提示应该显示的位置——在鼠标指针的尖端——并且我们调整背景“面板”的大小,使其实际上类似于大多数人接受的工具提示。
您可以添加自己的样式甚至动画以进一步优化所需的结果。
更多解释在下面代码中的 cmets 中:
<?xml version="2.0" encoding="utf-8" ?>
<!DOCTYPE svg SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<style>
#rich-tooltip {
pointer-events: none;
}
#rich-tooltip .panel {
fill: silver;
}
</style>
<defs>
<!-- the actual template, will be removed from the context and always shown as appended at the end of the document body, so that it is rendered above everything else. -->
<g id="rich-tooltip">
<rect class="panel" /><!-- just some decorative background -->
<g class="contents" /><!-- contents of an element's description will be added as child nodes of this element -->
</g>
</defs>
<circle cx="50" cy="50" r="25" fill="yellow">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">First circle</text></desc>
</circle>
<circle cx="70" cy="50" r="40" fill="green">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">Second circle</text></desc>
</circle>
<script>
const tooltip = document.getElementById("rich-tooltip");
tooltip.remove(); /// Initial state of the tooltip is "not shown" (removed from DOM tree)
var timeout; /// We only show the tooltip once the pointer settles and some time passes
const tooltip_delay = 1000; /// Delay before showing the tooltip once pointer settles
var last_tooltip_ev; /// Auxilary variable to be able to calculate movement after showing the tooltip, so we don't remove it immediately but only once the pointer actually moved substantially, this is just a nice touch, not otherwise crucial
const remove_tooltip_move_threshold = 10; /// How many user units (pixels, normall) the cursor may move before tooltip is hidden
function on_mouse_move_event(ev) {
if(document.contains(tooltip)) { /// Is the tooltip being shown?
if(last_tooltip_ev) {
if(((x, y) => Math.sqrt(x * x + y * y))(ev.clientX - last_tooltip_ev.clientX, ev.clientY - last_tooltip_ev.clientY) >= remove_tooltip_move_threshold) { /// has the pointer moved far enough from where the tooltip was originally shown?
tooltip.remove(); /// Hide the tooltip
}
}
} else {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(show_tooltip, tooltip_delay, ev);
}
}
function show_tooltip(ev) {
const desc = ev.target.querySelector(":scope > desc");
if(!desc) { /// Does the element that is under the pointer even have a description?
tooltip.remove(); /// Hide the tooltip (ignoring the fact it may not be showing in the first place)
return;
}
document.documentElement.appendChild(tooltip);
const desc_range = document.createRange();
desc_range.selectNodeContents(desc); /// Select all children of the description element, as `desc_range`
const contents = tooltip.querySelector(".contents");
const contents_range = document.createRange();
contents_range.selectNodeContents(contents); /// Select all children of the tooltip contents element, as `contents_range`
contents_range.extractContents(); /// Empty tooltip contents
contents.appendChild(desc_range.cloneContents()); /// Fill contents with previously selected description. We _copy_ the description -- the original should legitimately stay where it was
const panel = tooltip.querySelector("rect.panel");
panel.setAttribute("width", contents.getBBox().width);
panel.setAttribute("height", contents.getBBox().height); /// "Stretch" the panel so that it covers the tooltip contents
const pt = document.documentElement.createSVGPoint();
pt.x = ev.clientX;
pt.y = ev.clientY;
const view_pt = pt.matrixTransform(document.documentElement.getScreenCTM().inverse()); /// We need to convert mouse pointer coordinates to the SVG viewbox coordinates
tooltip.setAttribute("transform", `translate(${view_pt.x} ${view_pt.y})`); /// Move the tooltip to appropriate position
last_tooltip_ev = ev; /// Save the event to be able to calculate distance later (above)
}
addEventListener("mouseover", function(ev) { /// When the pointer gets over an element...
ev.target.addEventListener("mousemove", on_mouse_move_event); /// We will be tracking pointer movements to trigger timeout for showing the tooltip
ev.target.addEventListener("mouseout", function() { /// Once the pointer gets anywhere but the element itself -- like over its children or other elements...
ev.target.removeEventListener("mouseout", on_mouse_move_event); /// Cancel the whole mousemove business, the behavior will be setup by whatever element the mouse pointer gets over next anyway
}, { once: true }); /// Once, for this element, everything else will be setup by another call for "mouseover" listener
});
</script>
</svg>
如果没有超时触发等,代码会更简单,但可能是经过深思熟虑且用户友好的工具提示实现将使用延迟并补偿杂散的指针移动,所以我认为保留一些骨架框架是有意义的放置这些内容,并展示如何实施它们。
这无论如何都是相当理想的,因为您每次只使用一组侦听器——您不需要为要跟踪的每个元素分配侦听器。如果元素有描述,此脚本将确保显示工具提示,仅此而已。暂时,我们确实将mouseout 侦听器分配给一个元素,但通常只有一个侦听器在任何时间点只分配给一个元素——一旦指针离开元素,侦听器就会被删除(并且其他东西会重新分配)它的另一个实例,但这很好)。