mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
onglets : les porteurs du nom dans leur famille, proches de l'autre côté réglables
Un enfant d'un membre qui porte le nom d'un onglet entre dans cet onglet (nom de naissance d'abord, homonymes sans lien laissés dehors), avec son couple ; l'autre parent est estompé. Nouveau réglage d'affichage : proches de l'autre côté à 0, 1, 2 ou 3 liens, 1 par défaut. Libellés des réglages d'affichage réécrits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09b2acf4fe
commit
642b6020c3
7 files changed
+177
-25
No files matched your search
@@ -585,6 +585,9 @@ function App() {
|
|||||||
const [familyTabs, setFamilyTabsState] = useState(() => { try { return localStorage.getItem('parenthese.familyTabs') !== 'off' } catch { return true } })
|
const [familyTabs, setFamilyTabsState] = useState(() => { try { return localStorage.getItem('parenthese.familyTabs') !== 'off' } catch { return true } })
|
||||||
const setFamilyTabs = useCallback((on) => { setFamilyTabsState(on); try { localStorage.setItem('parenthese.familyTabs', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, [])
|
const setFamilyTabs = useCallback((on) => { setFamilyTabsState(on); try { localStorage.setItem('parenthese.familyTabs', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, [])
|
||||||
// Nom de famille sous le prénom dans l'arbre, celui de naissance s'il est connu (#168). Réglage de ce navigateur.
|
// Nom de famille sous le prénom dans l'arbre, celui de naissance s'il est connu (#168). Réglage de ce navigateur.
|
||||||
|
// Proches des autres familles dessinés, estompés, autour d'un onglet : 0 (ma ligne seulement) à 3 liens
|
||||||
|
const [tabKinDepth, setTabKinDepthState] = useState(() => { try { const v = localStorage.getItem('parenthese.tabKinDepth'); return ['0', '1', '2', '3'].includes(v) ? Number(v) : 1 } catch { return 1 } })
|
||||||
|
const setTabKinDepth = useCallback((d) => { setTabKinDepthState(d); try { localStorage.setItem('parenthese.tabKinDepth', String(d)) } catch { /* navigateur sans stockage */ } }, [])
|
||||||
const [fullNames, setFullNamesState] = useState(() => { try { return localStorage.getItem('parenthese.fullNames') === 'on' } catch { return false } })
|
const [fullNames, setFullNamesState] = useState(() => { try { return localStorage.getItem('parenthese.fullNames') === 'on' } catch { return false } })
|
||||||
const setFullNames = useCallback((on) => { setFullNamesState(on); try { localStorage.setItem('parenthese.fullNames', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, [])
|
const setFullNames = useCallback((on) => { setFullNamesState(on); try { localStorage.setItem('parenthese.fullNames', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, [])
|
||||||
// Premiers visages : fenêtre ouverte après un import GEDCOM, ou depuis les paramètres de l'arbre
|
// Premiers visages : fenêtre ouverte après un import GEDCOM, ou depuis les paramètres de l'arbre
|
||||||
@@ -3571,6 +3574,7 @@ function App() {
|
|||||||
reorderEnabled={canReorderSiblings}
|
reorderEnabled={canReorderSiblings}
|
||||||
onReorderSiblings={handleReorderSiblings}
|
onReorderSiblings={handleReorderSiblings}
|
||||||
familyTabs={familyTabs}
|
familyTabs={familyTabs}
|
||||||
|
tabKinDepth={tabKinDepth}
|
||||||
fullNames={fullNames}
|
fullNames={fullNames}
|
||||||
annotationViewRef={annotationViewRef}
|
annotationViewRef={annotationViewRef}
|
||||||
onAnnotationsAnchored={persistAnnotationAnchors}
|
onAnnotationsAnchored={persistAnnotationAnchors}
|
||||||
@@ -3752,6 +3756,8 @@ function App() {
|
|||||||
onClose={() => setAdminPanelVisible(false)}
|
onClose={() => setAdminPanelVisible(false)}
|
||||||
familyTabs={familyTabs}
|
familyTabs={familyTabs}
|
||||||
onFamilyTabsChange={setFamilyTabs}
|
onFamilyTabsChange={setFamilyTabs}
|
||||||
|
tabKinDepth={tabKinDepth}
|
||||||
|
onTabKinDepthChange={setTabKinDepth}
|
||||||
fullNames={fullNames}
|
fullNames={fullNames}
|
||||||
onFullNamesChange={setFullNames}
|
onFullNamesChange={setFullNames}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const TREE_ICONS = ['🌳', '🌿', '🌻', '🏡', '❤️', '🧡', '⭐', '
|
|||||||
const AdminPanel = ({
|
const AdminPanel = ({
|
||||||
familyTabs = true,
|
familyTabs = true,
|
||||||
onFamilyTabsChange = null,
|
onFamilyTabsChange = null,
|
||||||
|
tabKinDepth = 1,
|
||||||
|
onTabKinDepthChange = null,
|
||||||
fullNames = false,
|
fullNames = false,
|
||||||
onFullNamesChange = null,
|
onFullNamesChange = null,
|
||||||
visible,
|
visible,
|
||||||
@@ -535,6 +537,7 @@ const AdminPanel = ({
|
|||||||
|
|
||||||
<fieldset className="ts-policy">
|
<fieldset className="ts-policy">
|
||||||
<legend className="pz-eyebrow">{t('Affichage')}</legend>
|
<legend className="pz-eyebrow">{t('Affichage')}</legend>
|
||||||
|
<p className="pz-small">{t('Réglages de ce navigateur.')}</p>
|
||||||
<label className={`ts-choice ${familyTabs ? 'is-active' : ''}`}>
|
<label className={`ts-choice ${familyTabs ? 'is-active' : ''}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -543,10 +546,21 @@ const AdminPanel = ({
|
|||||||
onChange={(event) => onFamilyTabsChange?.(event.target.checked)}
|
onChange={(event) => onFamilyTabsChange?.(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span className="ts-choice-text">
|
<span className="ts-choice-text">
|
||||||
<strong>{t('Les grandes familles par alliance dans des onglets')}</strong>
|
<strong>{t('Séparer les belles-familles en onglets')}</strong>
|
||||||
<small>{t("Une belle-famille plus large qu'un écran s'ouvre dans son propre onglet au lieu d'être dessinée dans l'arbre. Réglage de ce navigateur.")}</small>
|
<small>{t("Quand la famille d'un conjoint est trop large pour l'écran, elle s'ouvre dans un onglet en haut de l'arbre.")}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="pz-field">
|
||||||
|
<span className="pz-label" id="settingsTabKinLabel">{t("Dans un onglet, montrer aussi les proches de l'autre côté")}</span>
|
||||||
|
<div className="ts-icons" role="radiogroup" aria-labelledby="settingsTabKinLabel">
|
||||||
|
{[[0, t('Ma ligne seulement')], [1, t('1 lien')], [2, t('2 liens')], [3, t('3 liens')]].map(([d, text]) => (
|
||||||
|
<button key={d} type="button" role="radio" aria-checked={tabKinDepth === d} disabled={!familyTabs} className="ts-icon ts-icon--letter" onClick={() => onTabKinDepthChange?.(d)}>
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="pz-hint">{t('Un lien : un parent, un enfant ou un conjoint. Ils apparaissent en plus clair ; votre propre ligne reste toujours visible.')}</p>
|
||||||
|
</div>
|
||||||
<label className={`ts-choice ${fullNames ? 'is-active' : ''}`}>
|
<label className={`ts-choice ${fullNames ? 'is-active' : ''}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -555,8 +569,8 @@ const AdminPanel = ({
|
|||||||
onChange={(event) => onFullNamesChange?.(event.target.checked)}
|
onChange={(event) => onFullNamesChange?.(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span className="ts-choice-text">
|
<span className="ts-choice-text">
|
||||||
<strong>{t("Le nom de famille sous le prénom")}</strong>
|
<strong>{t('Afficher le nom de famille')}</strong>
|
||||||
<small>{t("Le nom de naissance quand il est connu, sinon le nom d'usage. Réglage de ce navigateur.")}</small>
|
<small>{t("Sous chaque prénom. Le nom de naissance s'il est connu.")}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getPersonMedias, getParents, getChildren, getPersonUnions, persons as g
|
|||||||
import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_NODE_DURATION, ENTRANCE_CAMERA_DURATION, ENTRANCE_CAMERA_FROM_SCALE, ENTRANCE_REDUCED_DURATION, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants'
|
import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_NODE_DURATION, ENTRANCE_CAMERA_DURATION, ENTRANCE_CAMERA_FROM_SCALE, ENTRANCE_REDUCED_DURATION, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants'
|
||||||
import { easeOutCubic } from './galaxy/utils'
|
import { easeOutCubic } from './galaxy/utils'
|
||||||
import { computeLayout, generateNodeRandomData, coupleBarMeta, restoreCoupleBarMeta } from './galaxy/elkLayout'
|
import { computeLayout, generateNodeRandomData, coupleBarMeta, restoreCoupleBarMeta } from './galaxy/elkLayout'
|
||||||
import { computeBlockLayout, familyTabsOf, lineToFamily, subgraphOf, takeRootLayout } from './galaxy/blockLayout'
|
import { computeBlockLayout, familyTabsOf, kinAround, lineToFamily, subgraphOf, takeRootLayout } from './galaxy/blockLayout'
|
||||||
import { hasGhosts, withGhosts } from '../utils/ghosts'
|
import { hasGhosts, withGhosts } from '../utils/ghosts'
|
||||||
|
|
||||||
// Placement par blocs familiaux par défaut (mesures : tools/layout-bench). ?layout=sugiyama = placement Sugiyama.
|
// Placement par blocs familiaux par défaut (mesures : tools/layout-bench). ?layout=sugiyama = placement Sugiyama.
|
||||||
@@ -77,7 +77,7 @@ function barTabs(tabs, activeKey) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, reorderEnabled = false, onReorderSiblings = null, familyTabs = true, fullNames = false, annotationViewRef = null, onAnnotationsAnchored = null, selfPersonId = null, ghosts = null, initialFamilyKey = null, onFamilyChange = null }) => {
|
const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, reorderEnabled = false, onReorderSiblings = null, familyTabs = true, tabKinDepth = 1, fullNames = false, annotationViewRef = null, onAnnotationsAnchored = null, selfPersonId = null, ghosts = null, initialFamilyKey = null, onFamilyChange = null }) => {
|
||||||
const canvasRef = useRef(null)
|
const canvasRef = useRef(null)
|
||||||
const layoutDataRef = useRef(null)
|
const layoutDataRef = useRef(null)
|
||||||
// Onglets de familles (réglage `familyTabs`) : la liste des familles de l'arbre, calculée une fois par
|
// Onglets de familles (réglage `familyTabs`) : la liste des familles de l'arbre, calculée une fois par
|
||||||
@@ -675,6 +675,12 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
|||||||
// Dans l'onglet d'une autre famille, la personne commune (« C'est moi », sinon la personne de départ) reste
|
// Dans l'onglet d'une autre famille, la personne commune (« C'est moi », sinon la personne de départ) reste
|
||||||
// là, reliée par sa ligne directe (Patch, 21/10 : « je veux me voir tout le temps »)
|
// là, reliée par sa ligne directe (Patch, 21/10 : « je veux me voir tout le temps »)
|
||||||
const line = tab?.personIds ? lineToFamily(selfPersonId ?? startRefRef.current ?? tabsCacheRef.current.tabs[0]?.resolvedRef, tab.personIds) : new Set()
|
const line = tab?.personIds ? lineToFamily(selfPersonId ?? startRefRef.current ?? tabsCacheRef.current.tabs[0]?.resolvedRef, tab.personIds) : new Set()
|
||||||
|
// Autour, estompés aussi : l'autre parent des porteurs du nom venus d'ailleurs, et les proches de l'autre côté
|
||||||
|
// jusqu'à la profondeur choisie dans les réglages (Patch, 06/11)
|
||||||
|
if (tab?.personIds) {
|
||||||
|
for (const id of tab.faded || []) line.add(id)
|
||||||
|
for (const id of kinAround(tab.personIds, tabKinDepth)) line.add(id)
|
||||||
|
}
|
||||||
const tabGraph = tab?.personIds ? subgraphOf(new Set([...tab.personIds, ...line])) : null
|
const tabGraph = tab?.personIds ? subgraphOf(new Set([...tab.personIds, ...line])) : null
|
||||||
const graph = hasGhosts(ghosts)
|
const graph = hasGhosts(ghosts)
|
||||||
? withGhosts(tabGraph || { persons: graphPersons, unions: graphUnions, filiations: graphFiliations }, ghosts, !tabGraph)
|
? withGhosts(tabGraph || { persons: graphPersons, unions: graphUnions, filiations: graphFiliations }, ghosts, !tabGraph)
|
||||||
@@ -875,7 +881,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
|||||||
pendingFrameRef.current = null
|
pendingFrameRef.current = null
|
||||||
requestAnimationFrame(() => zoomApiRef?.current?.focusPersons?.(ids))
|
requestAnimationFrame(() => zoomApiRef?.current?.focusPersons?.(ids))
|
||||||
}
|
}
|
||||||
}, [graphRevision, activeKey, familyTabs, selfPersonId, ghosts, requestTabs, cancelTabsRequest])
|
}, [graphRevision, activeKey, familyTabs, tabKinDepth, selfPersonId, ghosts, requestTabs, cancelTabsRequest])
|
||||||
|
|
||||||
// Onglet ouvert, remonté pour l'adresse de la page et le lien de partage ; null pour la famille de départ
|
// Onglet ouvert, remonté pour l'adresse de la page et le lien de partage ; null pour la famille de départ
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
.ts-icon--letter { width: auto; padding: 0 12px; border-radius: 999px; font: 500 13px var(--pz-sans); color: var(--pz-ink-2); }
|
.ts-icon--letter { width: auto; padding: 0 12px; border-radius: 999px; font: 500 13px var(--pz-sans); color: var(--pz-ink-2); }
|
||||||
.ts-icon[aria-checked="true"] { border-color: var(--pz-a500); background: var(--pz-a50); box-shadow: 0 0 0 3px var(--pz-a100); }
|
.ts-icon[aria-checked="true"] { border-color: var(--pz-a500); background: var(--pz-a50); box-shadow: 0 0 0 3px var(--pz-a100); }
|
||||||
.ts-icon:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
.ts-icon:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||||
|
.ts-icon:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
/* Cinq onglets : ils rétrécissent (texte sur deux lignes) au lieu de déborder du panneau sur un petit téléphone */
|
/* Cinq onglets : ils rétrécissent (texte sur deux lignes) au lieu de déborder du panneau sur un petit téléphone */
|
||||||
.ts-tabs .pz-tab { min-width: 0; }
|
.ts-tabs .pz-tab { min-width: 0; }
|
||||||
|
|||||||
@@ -854,34 +854,35 @@ export function subgraphOf(personIds, graphData = null) {
|
|||||||
return { persons, unions, filiations }
|
return { persons, unions, filiations }
|
||||||
}
|
}
|
||||||
|
|
||||||
// La ligne directe d'une personne vers une famille (Patch, 21/10 : « je veux me voir tout le temps ») : le plus
|
// La parenté d'un arbre : pour chaque personne, ses voisins (conjoints, parents, enfants), chacun avec les personnes
|
||||||
// court chemin de parenté (parents, enfants, conjoints) de `selfId` jusqu'au premier membre de `memberIds`,
|
// que ce pas fait dessiner en plus. Chaque pas entre un enfant et un parent garde l'autre parent (le fil part de
|
||||||
// sans fratries autour. Chaque pas d'un enfant à un parent garde ses deux parents (le fil part de leur union,
|
// leur union, et sans l'autre parent le placement dessine un conjoint « Inconnu »).
|
||||||
// et sans l'autre parent le placement dessine un conjoint « Inconnu »).
|
function kinshipOf(graphData = null) {
|
||||||
// Rend les personnes à ajouter (ni `selfId` déjà membre, ni chemin : ensemble vide).
|
|
||||||
export function lineToFamily(selfId, memberIds, graphData = null) {
|
|
||||||
const members = new Set([...memberIds].map(String))
|
|
||||||
const self = selfId != null ? String(selfId) : null
|
|
||||||
if (self == null || members.has(self)) return new Set()
|
|
||||||
const persons = graphData?.persons || globalPersons
|
|
||||||
if (!persons.some(p => String(p.id) === self)) return new Set()
|
|
||||||
const unions = graphData?.unions || globalUnions
|
const unions = graphData?.unions || globalUnions
|
||||||
const filiations = graphData?.filiations || globalFiliations
|
const filiations = graphData?.filiations || globalFiliations
|
||||||
const unionById = new Map(unions.map(u => [String(u.id), u]))
|
const unionById = new Map(unions.map(u => [String(u.id), u]))
|
||||||
// Voisins : [voisin, personnes que ce pas fait dessiner en plus]
|
|
||||||
const next = new Map()
|
const next = new Map()
|
||||||
const link = (a, b, extra = []) => { if (!next.has(a)) next.set(a, []); next.get(a).push([b, extra]) }
|
const link = (a, b, extra = []) => { if (!next.has(a)) next.set(a, []); next.get(a).push([b, extra]) }
|
||||||
|
const partnersOf = new Map()
|
||||||
for (const u of unions) {
|
for (const u of unions) {
|
||||||
const a = String(u.partner1Id), b = String(u.partner2Id)
|
const a = String(u.partner1Id), b = String(u.partner2Id)
|
||||||
link(a, b); link(b, a)
|
link(a, b); link(b, a)
|
||||||
|
if (!partnersOf.has(a)) partnersOf.set(a, new Set())
|
||||||
|
if (!partnersOf.has(b)) partnersOf.set(b, new Set())
|
||||||
|
partnersOf.get(a).add(b); partnersOf.get(b).add(a)
|
||||||
}
|
}
|
||||||
const parentsOf = new Map()
|
const parentsOf = new Map()
|
||||||
|
const childrenOf = new Map()
|
||||||
for (const f of filiations) {
|
for (const f of filiations) {
|
||||||
const child = String(f.childId)
|
const child = String(f.childId)
|
||||||
const u = f.unionId ? unionById.get(String(f.unionId)) : null
|
const u = f.unionId ? unionById.get(String(f.unionId)) : null
|
||||||
const parents = u ? [String(u.partner1Id), String(u.partner2Id)] : f.parentId != null ? [String(f.parentId)] : []
|
const parents = u ? [String(u.partner1Id), String(u.partner2Id)] : f.parentId != null ? [String(f.parentId)] : []
|
||||||
if (!parentsOf.has(child)) parentsOf.set(child, new Set())
|
if (!parentsOf.has(child)) parentsOf.set(child, new Set())
|
||||||
for (const p of parents) parentsOf.get(child).add(p)
|
for (const p of parents) {
|
||||||
|
parentsOf.get(child).add(p)
|
||||||
|
if (!childrenOf.has(p)) childrenOf.set(p, new Set())
|
||||||
|
childrenOf.get(p).add(child)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [child, parents] of parentsOf) {
|
for (const [child, parents] of parentsOf) {
|
||||||
for (const p of parents) {
|
for (const p of parents) {
|
||||||
@@ -889,6 +890,20 @@ export function lineToFamily(selfId, memberIds, graphData = null) {
|
|||||||
link(child, p, couple); link(p, child, couple)
|
link(child, p, couple); link(p, child, couple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { next, parentsOf, childrenOf, partnersOf }
|
||||||
|
}
|
||||||
|
|
||||||
|
// La ligne directe d'une personne vers une famille (Patch, 21/10 : « je veux me voir tout le temps ») : le plus
|
||||||
|
// court chemin de parenté (parents, enfants, conjoints) de `selfId` jusqu'au premier membre de `memberIds`,
|
||||||
|
// sans fratries autour, chaque pas d'un enfant à un parent gardant ses deux parents.
|
||||||
|
// Rend les personnes à ajouter (ni `selfId` déjà membre, ni chemin : ensemble vide).
|
||||||
|
export function lineToFamily(selfId, memberIds, graphData = null) {
|
||||||
|
const members = new Set([...memberIds].map(String))
|
||||||
|
const self = selfId != null ? String(selfId) : null
|
||||||
|
if (self == null || members.has(self)) return new Set()
|
||||||
|
const persons = graphData?.persons || globalPersons
|
||||||
|
if (!persons.some(p => String(p.id) === self)) return new Set()
|
||||||
|
const { next } = kinshipOf(graphData)
|
||||||
const from = new Map([[self, null]])
|
const from = new Map([[self, null]])
|
||||||
const queue = [self]
|
const queue = [self]
|
||||||
let hit = null
|
let hit = null
|
||||||
@@ -909,6 +924,60 @@ export function lineToFamily(selfId, memberIds, graphData = null) {
|
|||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Les porteurs du nom d'une famille (Patch, 06/11 : « si un gonz a un nom de famille il fait partie de la famille
|
||||||
|
// au même nom ») : un enfant d'un membre qui porte le nom de l'onglet en devient membre, puis ses enfants au même
|
||||||
|
// nom, de génération en génération. Nom de naissance s'il est connu, sinon nom d'usage (une femme mariée notée au
|
||||||
|
// nom de son mari reste chez elle). Seuls les enfants d'un membre comptent : un homonyme sans lien reste dehors.
|
||||||
|
// Le couple d'un porteur est dessiné entier (`added` prend ses conjoints) ; l'autre parent d'un porteur ajouté,
|
||||||
|
// sans le nom, est rendu à part (`coParents`) pour être estompé, comme un proche de l'autre côté.
|
||||||
|
const nomCle = (s) => (s || '').trim().toLocaleLowerCase('fr')
|
||||||
|
export function bearersOfName(memberIds, name, graphData = null) {
|
||||||
|
const added = new Set()
|
||||||
|
const coParents = new Set()
|
||||||
|
const target = nomCle(name)
|
||||||
|
if (!target) return { added, coParents }
|
||||||
|
const persons = graphData?.persons || globalPersons
|
||||||
|
const nameOf = new Map(persons.map(p => [String(p.id), nomCle(p.birthName || p.lastName)]))
|
||||||
|
const { parentsOf, childrenOf, partnersOf } = kinshipOf(graphData)
|
||||||
|
const members = new Set([...memberIds].map(String))
|
||||||
|
const bearers = []
|
||||||
|
const queue = [...members]
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
for (const c of childrenOf.get(queue[i]) || []) {
|
||||||
|
if (members.has(c) || added.has(c) || nameOf.get(c) !== target) continue
|
||||||
|
added.add(c); bearers.push(c); queue.push(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inTab = (id) => members.has(id) || added.has(id)
|
||||||
|
for (const b of bearers) for (const p of partnersOf.get(b) || []) if (!inTab(p)) added.add(p)
|
||||||
|
for (const b of bearers) for (const p of parentsOf.get(b) || []) if (!inTab(p)) coParents.add(p)
|
||||||
|
return { added, coParents }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les proches de l'autre côté d'un onglet (réglage « Proches des autres familles ») : les personnes à `depth` pas
|
||||||
|
// de parenté au plus d'un membre, chaque pas d'un enfant à un parent gardant l'autre parent. Estompées à l'écran.
|
||||||
|
export function kinAround(memberIds, depth, graphData = null) {
|
||||||
|
const around = new Set()
|
||||||
|
if (!(depth > 0)) return around
|
||||||
|
const members = new Set([...memberIds].map(String))
|
||||||
|
const { next } = kinshipOf(graphData)
|
||||||
|
const dist = new Map([...members].map(id => [id, 0]))
|
||||||
|
const queue = [...members]
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
const d = dist.get(queue[i])
|
||||||
|
if (d >= depth) continue
|
||||||
|
for (const [nb, extra] of next.get(queue[i]) || []) {
|
||||||
|
for (const e of extra) if (!members.has(e)) around.add(e)
|
||||||
|
if (dist.has(nb)) continue
|
||||||
|
dist.set(nb, d + 1)
|
||||||
|
around.add(nb)
|
||||||
|
queue.push(nb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of members) around.delete(id)
|
||||||
|
return around
|
||||||
|
}
|
||||||
|
|
||||||
// Toutes les familles d'un arbre, de proche en proche : l'arbre de la personne de départ, puis chaque grande
|
// Toutes les familles d'un arbre, de proche en proche : l'arbre de la personne de départ, puis chaque grande
|
||||||
// belle-famille qu'il cache (un onglet = ce bloc caché plus le conjoint charnière, posé avec lui comme
|
// belle-famille qu'il cache (un onglet = ce bloc caché plus le conjoint charnière, posé avec lui comme
|
||||||
// référence), puis celles que ces onglets cachent à leur tour. Chaque personne est dans un seul onglet, sauf
|
// référence), puis celles que ces onglets cachent à leur tour. Chaque personne est dans un seul onglet, sauf
|
||||||
@@ -933,7 +1002,9 @@ export function familyTabsOf(graphData = null, referenceId = null) {
|
|||||||
const key = String(f.anchorId).replace(/^p-/, '')
|
const key = String(f.anchorId).replace(/^p-/, '')
|
||||||
if (seen.has(key)) continue
|
if (seen.has(key)) continue
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
tabs.push({ key, referenceId: key, parentKey: tab.key, personIds: new Set([...f.members, ...f.hinges]), hinges: f.hinges, label: f.label, count: f.count })
|
const base = new Set([...f.members, ...f.hinges])
|
||||||
|
const { added, coParents } = bearersOfName(base, f.label, graphData)
|
||||||
|
tabs.push({ key, referenceId: key, parentKey: tab.key, personIds: new Set([...base, ...added]), faded: coParents, hinges: f.hinges, label: f.label, count: f.count + added.size })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deux onglets au même nom (homonymes, ou une branche sortie de sa propre lignée) : sauf le plus grand, chacun
|
// Deux onglets au même nom (homonymes, ou une branche sortie de sa propre lignée) : sauf le plus grand, chacun
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { bearersOfName, kinAround } from './blockLayout'
|
||||||
|
|
||||||
|
// Henri Garnier (membre de l'onglet Garnier) et Odette, née Morel, notée au nom de son mari.
|
||||||
|
// Leurs enfants : Paul Garnier (marié à Julie Petit, une fille Lou Garnier) et Anne, au nom de sa mère.
|
||||||
|
// Un Garnier sans lien, ailleurs dans l'arbre.
|
||||||
|
const p = (id, firstName, lastName, birthName = null) => ({ id, firstName, lastName, birthName })
|
||||||
|
const graph = {
|
||||||
|
persons: [
|
||||||
|
p(1, 'Henri', 'Garnier'), p(2, 'Odette', 'Garnier', 'Morel'), p(3, 'Paul', 'Garnier'), p(4, 'Anne', 'Morel'),
|
||||||
|
p(5, 'Julie', 'Petit'), p(6, 'Lou', 'Garnier'), p(7, 'Jean', 'Garnier'), p(8, 'Marc', 'Morel'),
|
||||||
|
],
|
||||||
|
unions: [{ id: 'u1', partner1Id: 1, partner2Id: 2 }, { id: 'u2', partner1Id: 3, partner2Id: 5 }],
|
||||||
|
filiations: [
|
||||||
|
{ childId: 3, unionId: 'u1' }, { childId: 4, unionId: 'u1' }, { childId: 6, unionId: 'u2' }, { childId: 2, parentId: 8 },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bearersOfName', () => {
|
||||||
|
const { added, coParents } = bearersOfName(['1'], 'Garnier', graph)
|
||||||
|
it('prend les enfants au nom, de génération en génération, avec leurs conjoints', () => {
|
||||||
|
expect([...added].sort()).toEqual(['3', '5', '6'])
|
||||||
|
})
|
||||||
|
it("laisse dehors l'enfant au nom de sa mère, l'homonyme sans lien, et la mère notée au nom du mari", () => {
|
||||||
|
expect(added.has('4')).toBe(false)
|
||||||
|
expect(added.has('7')).toBe(false)
|
||||||
|
expect(added.has('2')).toBe(false)
|
||||||
|
})
|
||||||
|
it("rend l'autre parent à estomper", () => {
|
||||||
|
expect([...coParents]).toEqual(['2'])
|
||||||
|
})
|
||||||
|
it('ignore la casse du nom', () => {
|
||||||
|
expect(bearersOfName(['1'], 'GARNIER', graph).added.has('3')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('kinAround', () => {
|
||||||
|
it('ma ligne seulement : personne autour', () => {
|
||||||
|
expect(kinAround(['1'], 0, graph).size).toBe(0)
|
||||||
|
})
|
||||||
|
it('1 lien : conjoint et enfants, et l\'autre parent', () => {
|
||||||
|
expect([...kinAround(['1'], 1, graph)].sort()).toEqual(['2', '3', '4'])
|
||||||
|
})
|
||||||
|
it('2 liens : petits-enfants, conjoints des enfants, parents du conjoint', () => {
|
||||||
|
expect([...kinAround(['1'], 2, graph)].sort()).toEqual(['2', '3', '4', '5', '6', '8'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,10 +5,17 @@ export default {
|
|||||||
'Famille': 'Family',
|
'Famille': 'Family',
|
||||||
'Familles': 'Families',
|
'Familles': 'Families',
|
||||||
'Affichage': 'Display',
|
'Affichage': 'Display',
|
||||||
'Les grandes familles par alliance dans des onglets': 'Large in-law families in tabs',
|
'Réglages de ce navigateur.': 'Saved in this browser.',
|
||||||
"Une belle-famille plus large qu'un écran s'ouvre dans son propre onglet au lieu d'être dessinée dans l'arbre. Réglage de ce navigateur.": 'An in-law family wider than a screen opens in its own tab instead of being drawn in the tree. Saved in this browser.',
|
'Séparer les belles-familles en onglets': 'Put in-law families in tabs',
|
||||||
'Le nom de famille sous le prénom': 'Last name under the first name',
|
"Quand la famille d'un conjoint est trop large pour l'écran, elle s'ouvre dans un onglet en haut de l'arbre.": "When a spouse's family is too wide for the screen, it opens in a tab at the top of the tree.",
|
||||||
"Le nom de naissance quand il est connu, sinon le nom d'usage. Réglage de ce navigateur.": 'The maiden name when it is known, otherwise the current last name. Saved in this browser.',
|
"Dans un onglet, montrer aussi les proches de l'autre côté": 'In a tab, also show relatives from the other side',
|
||||||
|
'Ma ligne seulement': 'Only my line',
|
||||||
|
'1 lien': '1 link',
|
||||||
|
'2 liens': '2 links',
|
||||||
|
'3 liens': '3 links',
|
||||||
|
'Un lien : un parent, un enfant ou un conjoint. Ils apparaissent en plus clair ; votre propre ligne reste toujours visible.': 'A link is a parent, a child or a spouse. They appear lighter; your own line always stays visible.',
|
||||||
|
'Afficher le nom de famille': 'Show the last name',
|
||||||
|
"Sous chaque prénom. Le nom de naissance s'il est connu.": 'Under each first name. The maiden name when it is known.',
|
||||||
// Arbre sur canvas (components/galaxy/renderers.js)
|
// Arbre sur canvas (components/galaxy/renderers.js)
|
||||||
'Voir les parents': 'See parents',
|
'Voir les parents': 'See parents',
|
||||||
'Voir {firstName}': 'See {firstName}',
|
'Voir {firstName}': 'See {firstName}',
|
||||||
|
|||||||
Reference in new issue
Block a user