mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-21 14:50:58 +00:00
7c92e5ce18
The changes introduce three new JavaScript files that add comprehensive family tree visualization and analysis capabilities: 1. Advanced tree layouts with multiple visualization modes (hierarchical, radial, force-directed, timeline, and circular pedigree) 2. Detailed analytics and statistics with interactive charts 3. Advanced search functionality with tree highlighting and navigation features The additions provide users with richer ways to explore and analyze biblical family relationships.
916 lines
28 KiB
JavaScript
916 lines
28 KiB
JavaScript
/**
|
|
* Advanced Search and Navigation System for KJV Study Family Tree
|
|
* Provides comprehensive search capabilities with tree highlighting and breadcrumb navigation
|
|
*/
|
|
|
|
class FamilyTreeSearch {
|
|
constructor(familyData, treeVisualization) {
|
|
this.familyData = familyData;
|
|
this.treeVisualization = treeVisualization;
|
|
this.searchResults = [];
|
|
this.currentHighlights = [];
|
|
this.searchHistory = [];
|
|
this.bookmarks = JSON.parse(localStorage.getItem('familyTreeBookmarks')) || [];
|
|
this.breadcrumbs = [];
|
|
|
|
this.initializeSearch();
|
|
this.initializeNavigation();
|
|
this.loadBookmarks();
|
|
}
|
|
|
|
initializeSearch() {
|
|
// Create search interface
|
|
this.createSearchInterface();
|
|
this.setupSearchEventListeners();
|
|
this.buildSearchIndex();
|
|
}
|
|
|
|
createSearchInterface() {
|
|
const searchContainer = document.createElement('div');
|
|
searchContainer.className = 'family-search-container';
|
|
searchContainer.innerHTML = `
|
|
<div class="search-bar-container">
|
|
<div class="search-input-wrapper">
|
|
<input type="text" id="family-search-input" class="search-input"
|
|
placeholder="Search by name, title, or relationship...">
|
|
<button id="search-clear-btn" class="search-clear-btn" title="Clear search">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
<button id="advanced-search-toggle" class="advanced-search-toggle" title="Advanced search options">
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="search-filters">
|
|
<div class="filter-group">
|
|
<label>Search in:</label>
|
|
<label><input type="checkbox" name="searchFields" value="name" checked> Names</label>
|
|
<label><input type="checkbox" name="searchFields" value="title" checked> Titles</label>
|
|
<label><input type="checkbox" name="searchFields" value="description"> Descriptions</label>
|
|
<label><input type="checkbox" name="searchFields" value="verses"> Scripture References</label>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label>Gender:</label>
|
|
<label><input type="radio" name="genderFilter" value="all" checked> All</label>
|
|
<label><input type="radio" name="genderFilter" value="male"> Male</label>
|
|
<label><input type="radio" name="genderFilter" value="female"> Female</label>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label>Generation:</label>
|
|
<select id="generation-filter">
|
|
<option value="all">All Generations</option>
|
|
<option value="ancestors">Ancestors Only</option>
|
|
<option value="descendants">Descendants Only</option>
|
|
<option value="same">Same Generation</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-results-container">
|
|
<div class="search-results-header">
|
|
<span class="results-count">0 results</span>
|
|
<div class="result-actions">
|
|
<button id="highlight-all-btn" class="action-btn" title="Highlight all results in tree">
|
|
<i class="fas fa-highlighter"></i> Highlight All
|
|
</button>
|
|
<button id="export-results-btn" class="action-btn" title="Export search results">
|
|
<i class="fas fa-download"></i> Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="search-results-list" class="search-results-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="navigation-container">
|
|
<div class="breadcrumb-navigation">
|
|
<div class="breadcrumb-header">
|
|
<h4>Navigation Path</h4>
|
|
<button id="clear-breadcrumbs" class="clear-btn" title="Clear navigation history">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
<div id="breadcrumb-trail" class="breadcrumb-trail"></div>
|
|
</div>
|
|
|
|
<div class="quick-navigation">
|
|
<div class="nav-section">
|
|
<h4>Quick Access</h4>
|
|
<div class="quick-nav-buttons">
|
|
<button id="nav-root" class="nav-btn" title="Go to tree root">
|
|
<i class="fas fa-home"></i> Root
|
|
</button>
|
|
<button id="nav-back" class="nav-btn" title="Go back">
|
|
<i class="fas fa-arrow-left"></i> Back
|
|
</button>
|
|
<button id="nav-forward" class="nav-btn" title="Go forward">
|
|
<i class="fas fa-arrow-right"></i> Forward
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<h4>Bookmarks</h4>
|
|
<div class="bookmark-controls">
|
|
<button id="add-bookmark" class="bookmark-btn" title="Bookmark current person">
|
|
<i class="fas fa-bookmark"></i> Add
|
|
</button>
|
|
<button id="manage-bookmarks" class="bookmark-btn" title="Manage bookmarks">
|
|
<i class="fas fa-cog"></i> Manage
|
|
</button>
|
|
</div>
|
|
<div id="bookmarks-list" class="bookmarks-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Insert search container into the page
|
|
const familyViewer = document.querySelector('.family-viewer');
|
|
if (familyViewer) {
|
|
familyViewer.insertBefore(searchContainer, familyViewer.firstChild);
|
|
}
|
|
}
|
|
|
|
setupSearchEventListeners() {
|
|
const searchInput = document.getElementById('family-search-input');
|
|
const clearBtn = document.getElementById('search-clear-btn');
|
|
const advancedToggle = document.getElementById('advanced-search-toggle');
|
|
const highlightBtn = document.getElementById('highlight-all-btn');
|
|
const exportBtn = document.getElementById('export-results-btn');
|
|
|
|
// Search input with debouncing
|
|
let searchTimeout;
|
|
searchInput.addEventListener('input', (e) => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
this.performSearch(e.target.value);
|
|
}, 300);
|
|
});
|
|
|
|
// Clear search
|
|
clearBtn.addEventListener('click', () => {
|
|
searchInput.value = '';
|
|
this.clearSearch();
|
|
});
|
|
|
|
// Toggle advanced filters
|
|
advancedToggle.addEventListener('click', () => {
|
|
const filters = document.querySelector('.search-filters');
|
|
filters.classList.toggle('visible');
|
|
});
|
|
|
|
// Filter change events
|
|
document.querySelectorAll('input[name="searchFields"], input[name="genderFilter"]').forEach(input => {
|
|
input.addEventListener('change', () => {
|
|
if (searchInput.value) {
|
|
this.performSearch(searchInput.value);
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('generation-filter').addEventListener('change', () => {
|
|
if (searchInput.value) {
|
|
this.performSearch(searchInput.value);
|
|
}
|
|
});
|
|
|
|
// Result actions
|
|
highlightBtn.addEventListener('click', () => this.highlightAllResults());
|
|
exportBtn.addEventListener('click', () => this.exportSearchResults());
|
|
}
|
|
|
|
buildSearchIndex() {
|
|
this.searchIndex = {};
|
|
Object.entries(this.familyData).forEach(([id, person]) => {
|
|
this.searchIndex[id] = {
|
|
id: id,
|
|
name: person.name.toLowerCase(),
|
|
title: (person.title || '').toLowerCase(),
|
|
description: (person.description || '').toLowerCase(),
|
|
verses: this.extractVerseText(person.verses || []).toLowerCase(),
|
|
gender: this.determineGender(person),
|
|
searchText: [
|
|
person.name,
|
|
person.title || '',
|
|
person.description || '',
|
|
this.extractVerseText(person.verses || [])
|
|
].join(' ').toLowerCase()
|
|
};
|
|
});
|
|
}
|
|
|
|
performSearch(query) {
|
|
if (!query.trim()) {
|
|
this.clearSearch();
|
|
return;
|
|
}
|
|
|
|
const searchFields = this.getSelectedSearchFields();
|
|
const genderFilter = this.getSelectedGenderFilter();
|
|
const generationFilter = this.getGenerationFilter();
|
|
|
|
this.searchResults = this.executeSearch(query, searchFields, genderFilter, generationFilter);
|
|
this.displaySearchResults();
|
|
this.updateResultsCount();
|
|
|
|
// Add to search history
|
|
this.addToSearchHistory(query);
|
|
}
|
|
|
|
executeSearch(query, searchFields, genderFilter, generationFilter) {
|
|
const queryLower = query.toLowerCase();
|
|
const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0);
|
|
|
|
return Object.values(this.searchIndex).filter(person => {
|
|
// Gender filter
|
|
if (genderFilter !== 'all' && person.gender !== genderFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Generation filter (would need current person context)
|
|
if (generationFilter !== 'all') {
|
|
// Implementation depends on current tree state
|
|
// This is a placeholder for generation-based filtering
|
|
}
|
|
|
|
// Text search
|
|
const matchScore = this.calculateMatchScore(person, queryTerms, searchFields);
|
|
return matchScore > 0;
|
|
}).sort((a, b) => {
|
|
// Sort by relevance score
|
|
const scoreA = this.calculateMatchScore(a, queryTerms, searchFields);
|
|
const scoreB = this.calculateMatchScore(b, queryTerms, searchFields);
|
|
return scoreB - scoreA;
|
|
});
|
|
}
|
|
|
|
calculateMatchScore(person, queryTerms, searchFields) {
|
|
let score = 0;
|
|
|
|
queryTerms.forEach(term => {
|
|
searchFields.forEach(field => {
|
|
const fieldValue = person[field] || '';
|
|
if (fieldValue.includes(term)) {
|
|
// Exact name matches get highest score
|
|
if (field === 'name' && fieldValue === term) {
|
|
score += 10;
|
|
}
|
|
// Name contains term gets high score
|
|
else if (field === 'name') {
|
|
score += 5;
|
|
}
|
|
// Other fields get standard score
|
|
else {
|
|
score += 1;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return score;
|
|
}
|
|
|
|
displaySearchResults() {
|
|
const resultsList = document.getElementById('search-results-list');
|
|
resultsList.innerHTML = '';
|
|
|
|
if (this.searchResults.length === 0) {
|
|
resultsList.innerHTML = '<div class="no-results">No results found</div>';
|
|
return;
|
|
}
|
|
|
|
this.searchResults.forEach((result, index) => {
|
|
const person = this.familyData[result.id];
|
|
const resultElement = document.createElement('div');
|
|
resultElement.className = 'search-result-item';
|
|
resultElement.innerHTML = `
|
|
<div class="result-info">
|
|
<div class="result-name">${person.name}</div>
|
|
<div class="result-title">${person.title || 'Biblical Figure'}</div>
|
|
<div class="result-snippet">${this.createSearchSnippet(person)}</div>
|
|
</div>
|
|
<div class="result-actions">
|
|
<button class="select-person-btn" data-person-id="${result.id}" title="View this person">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="highlight-person-btn" data-person-id="${result.id}" title="Highlight in tree">
|
|
<i class="fas fa-highlighter"></i>
|
|
</button>
|
|
<button class="bookmark-person-btn" data-person-id="${result.id}" title="Bookmark this person">
|
|
<i class="fas fa-bookmark"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
resultElement.querySelector('.select-person-btn').addEventListener('click', () => {
|
|
this.selectPerson(result.id);
|
|
});
|
|
|
|
resultElement.querySelector('.highlight-person-btn').addEventListener('click', () => {
|
|
this.highlightPersonInTree(result.id);
|
|
});
|
|
|
|
resultElement.querySelector('.bookmark-person-btn').addEventListener('click', () => {
|
|
this.addBookmark(result.id);
|
|
});
|
|
|
|
resultsList.appendChild(resultElement);
|
|
});
|
|
}
|
|
|
|
createSearchSnippet(person) {
|
|
const snippetLength = 100;
|
|
let snippet = person.description || '';
|
|
|
|
if (snippet.length > snippetLength) {
|
|
snippet = snippet.substring(0, snippetLength) + '...';
|
|
}
|
|
|
|
return snippet || 'Biblical figure in genealogy';
|
|
}
|
|
|
|
// Navigation Methods
|
|
|
|
initializeNavigation() {
|
|
this.navigationHistory = [];
|
|
this.navigationIndex = -1;
|
|
this.setupNavigationEventListeners();
|
|
}
|
|
|
|
setupNavigationEventListeners() {
|
|
document.getElementById('nav-root').addEventListener('click', () => this.navigateToRoot());
|
|
document.getElementById('nav-back').addEventListener('click', () => this.navigateBack());
|
|
document.getElementById('nav-forward').addEventListener('click', () => this.navigateForward());
|
|
document.getElementById('clear-breadcrumbs').addEventListener('click', () => this.clearBreadcrumbs());
|
|
document.getElementById('add-bookmark').addEventListener('click', () => this.addCurrentBookmark());
|
|
document.getElementById('manage-bookmarks').addEventListener('click', () => this.openBookmarkManager());
|
|
}
|
|
|
|
addToBreadcrumbs(personId) {
|
|
const person = this.familyData[personId];
|
|
if (!person) return;
|
|
|
|
// Avoid duplicate consecutive entries
|
|
if (this.breadcrumbs.length > 0 && this.breadcrumbs[this.breadcrumbs.length - 1].id === personId) {
|
|
return;
|
|
}
|
|
|
|
this.breadcrumbs.push({
|
|
id: personId,
|
|
name: person.name,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Limit breadcrumb history
|
|
if (this.breadcrumbs.length > 10) {
|
|
this.breadcrumbs.shift();
|
|
}
|
|
|
|
this.updateBreadcrumbDisplay();
|
|
}
|
|
|
|
updateBreadcrumbDisplay() {
|
|
const breadcrumbTrail = document.getElementById('breadcrumb-trail');
|
|
breadcrumbTrail.innerHTML = '';
|
|
|
|
this.breadcrumbs.forEach((crumb, index) => {
|
|
const crumbElement = document.createElement('div');
|
|
crumbElement.className = 'breadcrumb-item';
|
|
crumbElement.innerHTML = `
|
|
<span class="breadcrumb-name">${crumb.name}</span>
|
|
<button class="breadcrumb-select" data-person-id="${crumb.id}" title="Go to ${crumb.name}">
|
|
<i class="fas fa-arrow-right"></i>
|
|
</button>
|
|
`;
|
|
|
|
crumbElement.querySelector('.breadcrumb-select').addEventListener('click', () => {
|
|
this.selectPerson(crumb.id);
|
|
});
|
|
|
|
breadcrumbTrail.appendChild(crumbElement);
|
|
|
|
// Add separator
|
|
if (index < this.breadcrumbs.length - 1) {
|
|
const separator = document.createElement('span');
|
|
separator.className = 'breadcrumb-separator';
|
|
separator.textContent = '→';
|
|
breadcrumbTrail.appendChild(separator);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bookmark Management
|
|
|
|
addBookmark(personId) {
|
|
const person = this.familyData[personId];
|
|
if (!person) return;
|
|
|
|
const bookmark = {
|
|
id: personId,
|
|
name: person.name,
|
|
title: person.title || 'Biblical Figure',
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
// Check if already bookmarked
|
|
if (!this.bookmarks.find(b => b.id === personId)) {
|
|
this.bookmarks.push(bookmark);
|
|
this.saveBookmarks();
|
|
this.updateBookmarksDisplay();
|
|
this.showNotification(`${person.name} added to bookmarks`);
|
|
} else {
|
|
this.showNotification(`${person.name} is already bookmarked`);
|
|
}
|
|
}
|
|
|
|
removeBookmark(personId) {
|
|
this.bookmarks = this.bookmarks.filter(b => b.id !== personId);
|
|
this.saveBookmarks();
|
|
this.updateBookmarksDisplay();
|
|
}
|
|
|
|
saveBookmarks() {
|
|
localStorage.setItem('familyTreeBookmarks', JSON.stringify(this.bookmarks));
|
|
}
|
|
|
|
loadBookmarks() {
|
|
this.updateBookmarksDisplay();
|
|
}
|
|
|
|
updateBookmarksDisplay() {
|
|
const bookmarksList = document.getElementById('bookmarks-list');
|
|
bookmarksList.innerHTML = '';
|
|
|
|
if (this.bookmarks.length === 0) {
|
|
bookmarksList.innerHTML = '<div class="no-bookmarks">No bookmarks yet</div>';
|
|
return;
|
|
}
|
|
|
|
this.bookmarks.forEach(bookmark => {
|
|
const bookmarkElement = document.createElement('div');
|
|
bookmarkElement.className = 'bookmark-item';
|
|
bookmarkElement.innerHTML = `
|
|
<div class="bookmark-info">
|
|
<div class="bookmark-name">${bookmark.name}</div>
|
|
<div class="bookmark-title">${bookmark.title}</div>
|
|
</div>
|
|
<div class="bookmark-actions">
|
|
<button class="bookmark-select" data-person-id="${bookmark.id}" title="Go to ${bookmark.name}">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="bookmark-remove" data-person-id="${bookmark.id}" title="Remove bookmark">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
bookmarkElement.querySelector('.bookmark-select').addEventListener('click', () => {
|
|
this.selectPerson(bookmark.id);
|
|
});
|
|
|
|
bookmarkElement.querySelector('.bookmark-remove').addEventListener('click', () => {
|
|
this.removeBookmark(bookmark.id);
|
|
});
|
|
|
|
bookmarksList.appendChild(bookmarkElement);
|
|
});
|
|
}
|
|
|
|
// Tree Highlighting
|
|
|
|
highlightPersonInTree(personId) {
|
|
// Remove existing highlights
|
|
this.clearHighlights();
|
|
|
|
// Add new highlight
|
|
this.currentHighlights.push(personId);
|
|
|
|
// Update tree visualization
|
|
if (this.treeVisualization && this.treeVisualization.highlightNode) {
|
|
this.treeVisualization.highlightNode(personId);
|
|
}
|
|
}
|
|
|
|
highlightAllResults() {
|
|
this.clearHighlights();
|
|
|
|
this.currentHighlights = this.searchResults.map(result => result.id);
|
|
|
|
if (this.treeVisualization && this.treeVisualization.highlightNodes) {
|
|
this.treeVisualization.highlightNodes(this.currentHighlights);
|
|
}
|
|
}
|
|
|
|
clearHighlights() {
|
|
this.currentHighlights = [];
|
|
|
|
if (this.treeVisualization && this.treeVisualization.clearHighlights) {
|
|
this.treeVisualization.clearHighlights();
|
|
}
|
|
}
|
|
|
|
// Utility Methods
|
|
|
|
getSelectedSearchFields() {
|
|
const checkboxes = document.querySelectorAll('input[name="searchFields"]:checked');
|
|
return Array.from(checkboxes).map(cb => cb.value);
|
|
}
|
|
|
|
getSelectedGenderFilter() {
|
|
const radio = document.querySelector('input[name="genderFilter"]:checked');
|
|
return radio ? radio.value : 'all';
|
|
}
|
|
|
|
getGenerationFilter() {
|
|
const select = document.getElementById('generation-filter');
|
|
return select ? select.value : 'all';
|
|
}
|
|
|
|
extractVerseText(verses) {
|
|
if (!Array.isArray(verses)) return '';
|
|
return verses.map(verse => verse.text || '').join(' ');
|
|
}
|
|
|
|
determineGender(person) {
|
|
const name = person.name.toLowerCase();
|
|
const femaleNames = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'mary', 'elizabeth'];
|
|
return femaleNames.includes(name) ? 'female' : 'male';
|
|
}
|
|
|
|
updateResultsCount() {
|
|
const countElement = document.querySelector('.results-count');
|
|
if (countElement) {
|
|
countElement.textContent = `${this.searchResults.length} result${this.searchResults.length !== 1 ? 's' : ''}`;
|
|
}
|
|
}
|
|
|
|
clearSearch() {
|
|
this.searchResults = [];
|
|
this.displaySearchResults();
|
|
this.updateResultsCount();
|
|
this.clearHighlights();
|
|
}
|
|
|
|
addToSearchHistory(query) {
|
|
if (!this.searchHistory.includes(query)) {
|
|
this.searchHistory.unshift(query);
|
|
if (this.searchHistory.length > 20) {
|
|
this.searchHistory.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
showNotification(message) {
|
|
// Simple notification system
|
|
const notification = document.createElement('div');
|
|
notification.className = 'search-notification';
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
exportSearchResults() {
|
|
if (this.searchResults.length === 0) {
|
|
this.showNotification('No search results to export');
|
|
return;
|
|
}
|
|
|
|
const csvContent = this.generateSearchResultsCSV();
|
|
this.downloadCSV(csvContent, 'family_tree_search_results.csv');
|
|
}
|
|
|
|
generateSearchResultsCSV() {
|
|
const headers = ['Name', 'Title', 'Description', 'Birth Year', 'Death Year'];
|
|
const rows = [headers];
|
|
|
|
this.searchResults.forEach(result => {
|
|
const person = this.familyData[result.id];
|
|
rows.push([
|
|
person.name,
|
|
person.title || '',
|
|
person.description || '',
|
|
person.birth_year || '',
|
|
person.death_year || ''
|
|
]);
|
|
});
|
|
|
|
return rows.map(row => row.map(field => `"${field}"`).join(',')).join('\n');
|
|
}
|
|
|
|
downloadCSV(content, filename) {
|
|
const blob = new Blob([content], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Public API
|
|
|
|
selectPerson(personId) {
|
|
this.addToBreadcrumbs(personId);
|
|
|
|
// Call the external select person function
|
|
if (window.selectPerson) {
|
|
window.selectPerson(personId);
|
|
}
|
|
}
|
|
|
|
getCurrentHighlights() {
|
|
return [...this.currentHighlights];
|
|
}
|
|
|
|
getSearchResults() {
|
|
return [...this.searchResults];
|
|
}
|
|
|
|
setTreeVisualization(treeViz) {
|
|
this.treeVisualization = treeViz;
|
|
}
|
|
}
|
|
|
|
// CSS Styles for Search Interface
|
|
const searchStyles = `
|
|
<style>
|
|
.family-search-container {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.search-bar-container {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.search-input-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
padding: 10px 15px;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 25px;
|
|
font-size: 14px;
|
|
outline: none;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
.search-input:focus {
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.search-clear-btn, .advanced-search-toggle {
|
|
margin-left: 10px;
|
|
padding: 10px;
|
|
border: none;
|
|
background: #6c757d;
|
|
color: white;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.search-clear-btn:hover, .advanced-search-toggle:hover {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.search-filters {
|
|
display: none;
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border: 1px solid #dee2e6;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.search-filters.visible {
|
|
display: block;
|
|
}
|
|
|
|
.filter-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.filter-group label {
|
|
margin-right: 15px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.search-results-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.search-results-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px;
|
|
border-bottom: 1px solid #dee2e6;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.search-results-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.search-result-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px;
|
|
border-bottom: 1px solid #f1f3f4;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.search-result-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.result-name {
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
}
|
|
|
|
.result-title {
|
|
font-size: 13px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.result-snippet {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.result-actions {
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
|
|
.result-actions button {
|
|
padding: 8px;
|
|
border: none;
|
|
background: #6c757d;
|
|
color: white;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.result-actions button:hover {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.navigation-container {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.breadcrumb-navigation, .quick-navigation {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.breadcrumb-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.breadcrumb-trail {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 5px 10px;
|
|
background: #f8f9fa;
|
|
border-radius: 15px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.nav-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.nav-section h4 {
|
|
margin-bottom: 10px;
|
|
color: #495057;
|
|
}
|
|
|
|
.quick-nav-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.nav-btn, .bookmark-btn {
|
|
padding: 8px 12px;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.nav-btn:hover, .bookmark-btn:hover {
|
|
background: #f8f9fa;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.bookmarks-list {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.bookmark-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px;
|
|
border-bottom: 1px solid #f1f3f4;
|
|
}
|
|
|
|
.bookmark-name {
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.bookmark-title {
|
|
font-size: 11px;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.search-notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #28a745;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
z-index: 1000;
|
|
animation: slideIn 0.3s ease;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); }
|
|
to { transform: translateX(0); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.navigation-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.search-filters {
|
|
padding: 10px;
|
|
}
|
|
|
|
.filter-group {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.breadcrumb-trail {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
// Inject styles
|
|
document.head.insertAdjacentHTML('beforeend', searchStyles);
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = FamilyTreeSearch;
|
|
} |