Если всплывающие подсказки пользовательского агента не собираются его сокращать, нужно будет реализовать некоторые функции самостоятельно.Я решил по-прежнему полагаться на декларативные элементы desc
, и мы можем использовать их тривиально, даже с подсказками для рендеринга.
В следующем документе SVG определение всплывающей подсказки используется в качестве шаблона и всякий раз, когда "мышь"«Указатель (все, что может генерировать события« mouse * », на самом деле) входит в элемент, мы извлекаем фрагмент документа (Range
), который является содержимым его элемента desc
, и копируем это содержимое в группу« contents »/графика всплывающей подсказки.Кроме того, мы рассчитываем позицию, в которой должна отображаться подсказка - на кончике указателя мыши - и изменяем размер фоновой «панели», чтобы она фактически напоминала то, что большинство людей принимает в качестве подсказок.
Вы можете добавить свой собственный стиль и даже анимацию для дальнейшего улучшения желаемого результата.
Более подробное объяснение приведено в комментариях к приведенному ниже коду:
<?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
элементу, но обычно это только один прослушиватель, назначенный только одному элементу в любой момент времени - как только указатель выходит из элемента, прослушиватель удаляется (и что-то еще переназначаетсяеще один пример, но это прекрасно).