Improve bio card: profile button, touch support, root-click navigation
Replace the "View profile" link with icon action buttons in the bio card top-right corner. On touch devices, single tap shows the bio card (with focus + profile buttons), double tap speed-focuses in the diagram. On desktop, click navigates the diagram and hover shows the bio card. Clicking the root/focused person now navigates to their profile page. Tapping the SVG background or the same card again dismisses the tooltip.
This commit is contained in:
@@ -155,8 +155,8 @@
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
min-width: 220px;
|
||||
max-width: 300px;
|
||||
min-width: 245px;
|
||||
max-width: 325px;
|
||||
font-size: 12px;
|
||||
color: #212529;
|
||||
pointer-events: auto;
|
||||
@@ -180,12 +180,21 @@
|
||||
.bio-header-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.bio-header-text {
|
||||
padding-right: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.bio-age {
|
||||
@@ -220,18 +229,46 @@
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.bio-link {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #4a90d9;
|
||||
text-decoration: none;
|
||||
font-size: 11px;
|
||||
/* Action buttons — top-right corner */
|
||||
.bio-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bio-link:hover {
|
||||
text-decoration: underline;
|
||||
.bio-action-btn,
|
||||
.bio-action-btn:visited {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: #f0f2f5;
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.bio-action-btn:hover {
|
||||
background: #4a90d9;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Focus button: only visible on touch devices */
|
||||
.bio-focus-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.bio-focus-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Zoom controls ── */
|
||||
|
||||
2
resources/js/full-diagram.min.js
vendored
2
resources/js/full-diagram.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ export default class Chart {
|
||||
this.zoomBehavior = zoomBehavior;
|
||||
|
||||
svg.on("zoom.tooltip", () => hideTooltip());
|
||||
svg.on("click.tooltip", () => hideTooltip());
|
||||
createZoomControls(ctr, svg, zoomBehavior);
|
||||
|
||||
const canvas = getCanvas(svg);
|
||||
@@ -46,12 +47,16 @@ export default class Chart {
|
||||
this.config
|
||||
);
|
||||
|
||||
// Click handler
|
||||
// Click handler — root person goes to profile, others reload diagram
|
||||
const baseUrl = this.baseUrl;
|
||||
const mainId = this.data.mainId;
|
||||
const onNodeClick = (data) => {
|
||||
hideTooltip();
|
||||
const url = baseUrl.replace("__XREF__", data.id);
|
||||
window.location.href = url;
|
||||
if (data.id === mainId && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
window.location.href = baseUrl.replace("__XREF__", data.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Draw connections first (behind cards)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* Profile picture displayed if available, otherwise a gendered silhouette.
|
||||
* Hover shows a rich bio card with dates, places, occupation, age.
|
||||
*/
|
||||
import { attachHoverBioCard } from "./overlay.js";
|
||||
import { attachHoverBioCard, showBioCard, hideTooltip } from "./overlay.js";
|
||||
|
||||
const isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
/**
|
||||
* Render a person card as an SVG group.
|
||||
@@ -29,11 +31,31 @@ export function renderPersonCard(parent, person, config, onClick, containerSelec
|
||||
.append("g")
|
||||
.attr("class", `person-card ${sexClass} ${rootClass}`.trim())
|
||||
.attr("transform", `translate(${person.x - w / 2}, ${person.y - h / 2})`)
|
||||
.style("cursor", "pointer")
|
||||
.on("click", (event) => {
|
||||
.style("cursor", "pointer");
|
||||
|
||||
// Touch: single tap → bio card, double tap → navigate diagram
|
||||
// Desktop: single click → navigate diagram (hover handles bio card)
|
||||
if (isTouch) {
|
||||
let lastTap = 0;
|
||||
g.on("click", function (event) {
|
||||
event.stopPropagation();
|
||||
const now = Date.now();
|
||||
if (now - lastTap < 350) {
|
||||
// Double tap → navigate
|
||||
hideTooltip();
|
||||
onClick({ id: person.id, data });
|
||||
} else {
|
||||
// Single tap → show bio card
|
||||
showBioCard(data, this, containerSelector, onClick, person.id);
|
||||
}
|
||||
lastTap = now;
|
||||
});
|
||||
} else {
|
||||
g.on("click", (event) => {
|
||||
event.stopPropagation();
|
||||
onClick({ id: person.id, data });
|
||||
});
|
||||
}
|
||||
|
||||
// "More ancestors" indicator — drawn first so card renders on top
|
||||
if (data.hasMoreAncestors) {
|
||||
@@ -197,9 +219,9 @@ export function renderPersonCard(parent, person, config, onClick, containerSelec
|
||||
.text(truncateText(subtitle, maxTextWidth));
|
||||
}
|
||||
|
||||
// Attach hover bio card
|
||||
if (containerSelector) {
|
||||
attachHoverBioCard(g, data, containerSelector);
|
||||
// Attach hover bio card (desktop only — touch uses tap)
|
||||
if (containerSelector && !isTouch) {
|
||||
attachHoverBioCard(g, data, containerSelector, onClick, person.id);
|
||||
}
|
||||
|
||||
return g;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { select } from "../d3.js";
|
||||
|
||||
let activeTooltip = null;
|
||||
let activePersonId = null;
|
||||
let hideTimer = null;
|
||||
|
||||
/** Get a translated string, with optional substitution. */
|
||||
@@ -27,8 +28,15 @@ function t(key, ...args) {
|
||||
* @param {object} data - Person data
|
||||
* @param {SVGElement} cardElement - The SVG card group element
|
||||
* @param {string} containerSelector
|
||||
* @param {Function} [onFocus] - Optional callback to focus this person in the diagram
|
||||
* @param {string} [personId] - Person ID for the focus callback
|
||||
*/
|
||||
export function showBioCard(data, cardElement, containerSelector) {
|
||||
export function showBioCard(data, cardElement, containerSelector, onFocus, personId) {
|
||||
// Toggle: tapping the same card again dismisses the tooltip
|
||||
if (activeTooltip && activePersonId === personId) {
|
||||
hideTooltip();
|
||||
return;
|
||||
}
|
||||
hideTooltip();
|
||||
|
||||
const container = select(containerSelector);
|
||||
@@ -78,14 +86,48 @@ export function showBioCard(data, cardElement, containerSelector) {
|
||||
addFact(facts, t("Occupation"), data.occupation);
|
||||
addFact(facts, t("Residence"), data.residence);
|
||||
|
||||
// Link to profile
|
||||
tooltip
|
||||
.append("a")
|
||||
.attr("href", data.url)
|
||||
.attr("class", "bio-link")
|
||||
.text(t("View profile") + " \u2192");
|
||||
// Action buttons — top-right corner
|
||||
const actions = tooltip.append("div").attr("class", "bio-actions");
|
||||
|
||||
// Focus in diagram button (navigates the chart to this person)
|
||||
if (onFocus && personId) {
|
||||
actions
|
||||
.append("button")
|
||||
.attr("type", "button")
|
||||
.attr("class", "bio-action-btn bio-focus-btn")
|
||||
.attr("title", t("Focus in diagram"))
|
||||
.html(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<circle cx="11" cy="11" r="8"/>' +
|
||||
'<line x1="21" y1="21" x2="16.65" y2="16.65"/>' +
|
||||
'<line x1="11" y1="8" x2="11" y2="14"/>' +
|
||||
'<line x1="8" y1="11" x2="14" y2="11"/>' +
|
||||
'</svg>'
|
||||
)
|
||||
.on("click", () => {
|
||||
hideTooltip();
|
||||
onFocus({ id: personId, data });
|
||||
});
|
||||
}
|
||||
|
||||
// View profile button (goes to webtrees individual page)
|
||||
if (data.url) {
|
||||
actions
|
||||
.append("a")
|
||||
.attr("href", data.url)
|
||||
.attr("class", "bio-action-btn bio-profile-btn")
|
||||
.attr("title", t("View profile"))
|
||||
.html(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>' +
|
||||
'<polyline points="15 3 21 3 21 9"/>' +
|
||||
'<line x1="10" y1="14" x2="21" y2="3"/>' +
|
||||
'</svg>'
|
||||
);
|
||||
}
|
||||
|
||||
activeTooltip = tooltip;
|
||||
activePersonId = personId || null;
|
||||
}
|
||||
|
||||
function addFact(container, label, value, place) {
|
||||
@@ -169,6 +211,7 @@ export function hideTooltip() {
|
||||
if (activeTooltip) {
|
||||
activeTooltip.remove();
|
||||
activeTooltip = null;
|
||||
activePersonId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +221,14 @@ export function hideTooltip() {
|
||||
* @param {d3.Selection} cardGroup - The SVG <g> for the person card
|
||||
* @param {object} data - Person data
|
||||
* @param {string} containerSelector
|
||||
* @param {Function} [onFocus] - Optional callback to focus this person in the diagram
|
||||
* @param {string} [personId] - Person ID for the focus callback
|
||||
*/
|
||||
export function attachHoverBioCard(cardGroup, data, containerSelector) {
|
||||
export function attachHoverBioCard(cardGroup, data, containerSelector, onFocus, personId) {
|
||||
cardGroup
|
||||
.on("mouseenter", function () {
|
||||
clearTimeout(hideTimer);
|
||||
showBioCard(data, this, containerSelector);
|
||||
showBioCard(data, this, containerSelector, onFocus, personId);
|
||||
})
|
||||
.on("mouseleave", () => {
|
||||
scheduleHide();
|
||||
|
||||
Reference in New Issue
Block a user