xref: /webtrees/app/Module/DescendancyModule.php (revision 6bdf767435631ad1dc27ec1ffd855d43dbdce907)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2017 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees\Module;
17
18use Fisharebest\Webtrees\Database;
19use Fisharebest\Webtrees\Family;
20use Fisharebest\Webtrees\Filter;
21use Fisharebest\Webtrees\I18N;
22use Fisharebest\Webtrees\Individual;
23use Fisharebest\Webtrees\Tree;
24
25/**
26 * Class DescendancyModule
27 */
28class DescendancyModule extends AbstractModule implements ModuleSidebarInterface {
29	/** {@inheritdoc} */
30	public function getTitle() {
31		return /* I18N: Name of a module/sidebar */
32			I18N::translate('Descendants');
33	}
34
35	/** {@inheritdoc} */
36	public function getDescription() {
37		return /* I18N: Description of the “Descendants” module */
38			I18N::translate('A sidebar showing the descendants of an individual.');
39	}
40
41	/**
42	 * This is a general purpose hook, allowing modules to respond to routes
43	 * of the form module.php?mod=FOO&mod_action=BAR
44	 *
45	 * @param string $mod_action
46	 */
47	public function modAction($mod_action) {
48		global $WT_TREE;
49
50		header('Content-Type: text/html; charset=UTF-8');
51
52		switch ($mod_action) {
53		case 'search':
54			$search = Filter::get('search');
55			echo $this->search($search, $WT_TREE);
56			break;
57		case 'descendants':
58			$individual = Individual::getInstance(Filter::get('xref', WT_REGEX_XREF), $WT_TREE);
59			if ($individual) {
60				echo $this->loadSpouses($individual, 1);
61			}
62			break;
63		default:
64			http_response_code(404);
65			break;
66		}
67	}
68
69	/** {@inheritdoc} */
70	public function defaultSidebarOrder() {
71		return 30;
72	}
73
74	/** {@inheritdoc} */
75	public function hasSidebarContent() {
76		return true;
77	}
78
79	/** {@inheritdoc} */
80	public function getSidebarAjaxContent() {
81		return '';
82	}
83
84	/**
85	 * Load this sidebar synchronously.
86	 *
87	 * @return string
88	 */
89	public function getSidebarContent() {
90		global $controller;
91
92		$controller->addInlineJavascript('
93			function dsearchQ() {
94				var query = $("#sb_desc_name").val();
95				if (query.length>1) {
96					$("#sb_desc_content").load("module.php?mod=' . $this->getName() . '&mod_action=search&search="+query);
97				}
98			}
99
100			$("#sb_desc_name").focus(function(){this.select();});
101			$("#sb_desc_name").blur(function(){if (this.value=="") this.value="' . I18N::translate('Search') . '";});
102			var dtimerid = null;
103			$("#sb_desc_name").keyup(function(e) {
104				if (dtimerid) window.clearTimeout(dtimerid);
105				dtimerid = window.setTimeout("dsearchQ()", 500);
106			});
107
108			$("#sb_desc_content").on("click", ".sb_desc_indi", function() {
109				var self = $(this),
110					state = self.children(".plusminus"),
111					target = self.siblings("div");
112				if(state.hasClass("icon-plus")) {
113					if (jQuery.trim(target.html())) {
114						target.show("fast"); // already got content so just show it
115					} else {
116						target
117							.hide()
118							.load(self.attr("href"), function(response, status, xhr) {
119								if(status == "success" && response !== "") {
120									target.show("fast");
121								}
122							})
123					}
124				} else {
125					target.hide("fast");
126				}
127				state.toggleClass("icon-minus icon-plus");
128				return false;
129			});
130		');
131
132		return
133			'<form method="post" action="module.php?mod=' . $this->getName() . '&amp;mod_action=search" onsubmit="return false;">' .
134			'<input type="search" name="sb_desc_name" id="sb_desc_name" placeholder="' . I18N::translate('Search') . '">' .
135			'</form>' .
136			'<div id="sb_desc_content">' .
137			'<ul>' . $this->getPersonLi($controller->record, 1) . '</ul>' .
138			'</div>';
139	}
140
141	/**
142	 * Format an individual in a list.
143	 *
144	 * @param Individual $person
145	 * @param int        $generations
146	 *
147	 * @return string
148	 */
149	public function getPersonLi(Individual $person, $generations = 0) {
150		$icon     = $generations > 0 ? 'icon-minus' : 'icon-plus';
151		$lifespan = $person->canShow() ? '(' . $person->getLifeSpan() . ')' : '';
152		$spouses  = $generations > 0 ? $this->loadSpouses($person, 0) : '';
153
154		return
155			'<li class="sb_desc_indi_li">' .
156			'<a class="sb_desc_indi" href="module.php?mod=' . $this->getName() . '&amp;mod_action=descendants&amp;xref=' . $person->getXref() . '">' .
157			'<i class="plusminus ' . $icon . '"></i>' .
158			$person->getSexImage() . $person->getFullName() . $lifespan .
159			'</a>' .
160			'<a class="icon-button_indi" href="' . $person->getHtmlUrl() . '"></a>' .
161			'<div>' . $spouses . '</div>' .
162			'</li>';
163	}
164
165	/**
166	 * Format a family in a list.
167	 *
168	 * @param Family     $family
169	 * @param Individual $person
170	 * @param int        $generations
171	 *
172	 * @return string
173	 */
174	public function getFamilyLi(Family $family, Individual $person, $generations = 0) {
175		$spouse = $family->getSpouse($person);
176		if ($spouse) {
177			$spouse_name = $spouse->getSexImage() . $spouse->getFullName();
178			$spouse_link = '<a class="icon-button_indi" href="' . $spouse->getHtmlUrl() . '"></a>';
179		} else {
180			$spouse_name = '';
181			$spouse_link = '';
182		}
183
184		$marryear = $family->getMarriageYear();
185		$marr     = $marryear ? '<i class="icon-rings"></i>' . $marryear : '';
186
187		return
188			'<li class="sb_desc_indi_li">' .
189			'<a class="sb_desc_indi" href="#"><i class="plusminus icon-minus"></i>' . $spouse_name . $marr . '</a>' .
190			$spouse_link .
191			'<a href="' . $family->getHtmlUrl() . '" class="icon-button_family"></a>' .
192		 '<div>' . $this->loadChildren($family, $generations) . '</div>' .
193			'</li>';
194	}
195
196	/**
197	 * Respond to an autocomplete search request.
198	 *
199	 * @param string $query Search for this term
200	 * @param Tree   $tree  Search in this tree
201	 *
202	 * @return string
203	 */
204	public function search($query, Tree $tree) {
205		if (strlen($query) < 2) {
206			return '';
207		}
208
209		$rows = Database::prepare(
210			"SELECT i_id AS xref" .
211			" FROM `##individuals`" .
212			" JOIN `##name` ON i_id = n_id AND i_file = n_file" .
213			" WHERE n_sort LIKE CONCAT('%', :query, '%') AND i_file = :tree_id" .
214			" ORDER BY n_sort"
215		)->execute([
216			'query'   => $query,
217			'tree_id' => $tree->getTreeId(),
218		])->fetchAll();
219
220		$out = '';
221		foreach ($rows as $row) {
222			$person = Individual::getInstance($row->xref, $tree);
223			if ($person && $person->canShowName()) {
224				$out .= $this->getPersonLi($person);
225			}
226		}
227		if ($out) {
228			return '<ul>' . $out . '</ul>';
229		} else {
230			return '';
231		}
232	}
233
234	/**
235	 * Display spouses.
236	 *
237	 * @param Individual $person
238	 * @param int        $generations
239	 *
240	 * @return string
241	 */
242	public function loadSpouses(Individual $person, $generations) {
243		$out = '';
244		if ($person && $person->canShow()) {
245			foreach ($person->getSpouseFamilies() as $family) {
246				$out .= $this->getFamilyLi($family, $person, $generations - 1);
247			}
248		}
249		if ($out) {
250			return '<ul>' . $out . '</ul>';
251		} else {
252			return '';
253		}
254	}
255
256	/**
257	 * Display descendants.
258	 *
259	 * @param Family $family
260	 * @param int    $generations
261	 *
262	 * @return string
263	 */
264	public function loadChildren(Family $family, $generations) {
265		$out = '';
266		if ($family->canShow()) {
267			$children = $family->getChildren();
268			if ($children) {
269				foreach ($children as $child) {
270					$out .= $this->getPersonLi($child, $generations - 1);
271				}
272			} else {
273				$out .= '<li class="sb_desc_none">' . I18N::translate('No children') . '</li>';
274			}
275		}
276		if ($out) {
277			return '<ul>' . $out . '</ul>';
278		} else {
279			return '';
280		}
281	}
282}
283