xref: /webtrees/resources/js/treeview.js (revision 08362db417156410ee02c6391bd9eb83a2df65fc)
1/**
2 * webtrees: online genealogy
3 * Copyright (C) 2019 webtrees development team
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 */
15
16function TreeViewHandler(treeview_instance, ged) {
17	var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
18
19	this.treeview = $("#" + treeview_instance + "_in");
20	this.loadingImage = $("#" + treeview_instance + "_loading");
21	this.toolbox = $("#tv_tools");
22	this.buttons = $(".tv_button:first", this.toolbox);
23	this.zoom = 100; // in percent
24	this.boxWidth = 180; // default family box width
25	this.boxExpandedWidth = 250; // default expanded family box width
26	this.cookieDays = 3; // lifetime of preferences memory, in days
27	this.ajaxDetails = document.getElementById(treeview_instance + "_out").dataset.urlDetails + "&instance=" + encodeURIComponent(treeview_instance);
28	this.ajaxPersons = document.getElementById(treeview_instance + "_out").dataset.urlIndividuals + "&instance=" + encodeURIComponent(treeview_instance);
29
30	this.container = this.treeview.parent(); // Store the container element ("#" + treeview_instance + "_out")
31	this.auto_box_width = false;
32	this.updating = false;
33
34	// Restore user preferences
35	if (readCookie("compact") === "true") {
36		tv.compact();
37	}
38
39	///////////////////////////////////////////////
40	// Based on https://codepen.io/chriscoyier/pen/zdsty
41	(function($) {
42		$.fn.drags = function(opt) {
43			var $el = this;
44
45			return $el.css('cursor', 'move').on("mousedown", function(e) {
46				var $drag = $(this);
47				var drg_h = $drag.outerHeight();
48				var drg_w = $drag.outerWidth();
49				var pos_y = $drag.offset().top + drg_h - e.pageY;
50				var pos_x = $drag.offset().left + drg_w - e.pageX;
51
52				$drag.addClass('draggable');
53
54				$(document)
55					.on("mousemove", function(e) {
56						$('.draggable').offset({
57							top:e.pageY + pos_y - drg_h,
58							left:e.pageX + pos_x - drg_w
59						}).on("mouseup", function() {
60							$drag.removeClass('draggable');
61						});
62					}).on("mouseup", function() {
63						$drag.removeClass('draggable');
64						tv.updateTree();
65					});
66				e.preventDefault();
67			});
68
69		}
70	})(jQuery);
71
72	tv.treeview.drags();
73	///////////////////////////////////////////////
74
75	// Add click handlers to buttons
76	tv.toolbox.find("#tvbCompact").each(function (index, tvCompact) {
77		tvCompact.onclick = function () {
78			tv.compact();
79		};
80	});
81	// If we click the "hide/show all partners" button, toggle the setting before reloading the page
82	tv.toolbox.find("#tvbAllPartners").each(function (index, tvAllPartners) {
83		tvAllPartners.onclick = function () {
84			createCookie("allPartners", readCookie("allPartners") === "true" ? "false" : "true", tv.cookieDays);
85			document.location = document.location;
86		};
87	});
88	tv.toolbox.find("#tvbOpen").each(function (index, tvbOpen) {
89		var b = $(tvbOpen, tv.toolbox);
90		tvbOpen.onclick = function () {
91			b.addClass("tvPressed");
92			tv.setLoading();
93			var e = jQuery.Event("click");
94			tv.treeview.find(".tv_box:not(.boxExpanded)").each(function (index, box) {
95				var pos = $(box, tv.treeview).offset();
96				if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
97					tv.expandBox(box, e);
98				}
99			});
100			b.removeClass("tvPressed");
101			tv.setComplete();
102		};
103	});
104	tv.toolbox.find("#tvbClose").each(function (index, tvbClose) {
105		var b = $(tvbClose, tv.toolbox);
106		tvbClose.onclick = function () {
107			b.addClass("tvPressed");
108			tv.setLoading();
109			tv.treeview.find(".tv_box.boxExpanded").each(function (index, box) {
110				$(box).css("display", "none").removeClass("boxExpanded").parent().find(".tv_box.collapsedContent").css("display", "block");
111			});
112			b.removeClass("tvPressed");
113			tv.setComplete();
114		};
115	});
116
117	tv.centerOnRoot(); // fire ajax update if needed, which call setComplete() when all is loaded
118}
119/**
120 * Class TreeView setLoading method
121 */
122TreeViewHandler.prototype.setLoading = function () {
123	this.treeview.css("cursor", "wait");
124	this.loadingImage.css("display", "block");
125};
126/**
127 * Class TreeView setComplete  method
128 */
129TreeViewHandler.prototype.setComplete = function () {
130	this.treeview.css("cursor", "move");
131	this.loadingImage.css("display", "none");
132};
133
134/**
135 * Class TreeView getSize  method
136 * Store the viewport current size
137 */
138TreeViewHandler.prototype.getSize = function () {
139	var tv = this;
140	// retrieve the current container bounding box
141	var container = tv.container.parent();
142	var offset = container.offset();
143	tv.leftMin = offset.left;
144	tv.leftMax = tv.leftMin + container.innerWidth();
145	tv.topMin = offset.top;
146	tv.topMax = tv.topMin + container.innerHeight();
147	/*
148	 var frm = $("#tvTreeBorder");
149	 tv.treeview.css("width", frm.width());
150	 tv.treeview.css("height", frm.height());*/
151};
152
153/**
154 * Class TreeView updateTree  method
155 * Perform ajax requests to complete the tree after drag
156 * param boolean @center center on root person when done
157 */
158TreeViewHandler.prototype.updateTree = function (center, button) {
159	var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
160	var to_load = [];
161	var elts = [];
162	this.getSize();
163
164	// check which td with datafld attribute are within the container bounding box
165	// and therefore need to be dynamically loaded
166	tv.treeview.find("td[abbr]").each(function (index, el) {
167		el = $(el, tv.treeview);
168		var pos = el.offset();
169		if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
170			to_load.push(el.attr("abbr"));
171			elts.push(el);
172		}
173	});
174	// if some boxes need update, we perform an ajax request
175	if (to_load.length > 0) {
176		tv.updating = true;
177		tv.setLoading();
178		jQuery.ajax({
179			url: tv.ajaxPersons,
180			dataType: "json",
181			data: "q=" + to_load.join(";"),
182			success: function (ret) {
183				var nb = elts.length;
184				var root_element = $(".rootPerson", this.treeview);
185				var l = root_element.offset().left;
186				for (var i = 0; i < nb; i++) {
187					elts[i].removeAttr("abbr").html(ret[i]);
188				}
189				// we now ajust the draggable treeview size to its content size
190				tv.getSize();
191			},
192			complete: function () {
193				if (tv.treeview.find("td[abbr]").length) {
194					tv.updateTree(center, button); // recursive call
195				}
196				// the added boxes need that in mode compact boxes
197				if (tv.auto_box_width) {
198					tv.treeview.find(".tv_box").css("width", "auto");
199				}
200				tv.updating = true; // avoid an unuseful recursive call when all requested persons are loaded
201				if (center) {
202					tv.centerOnRoot();
203				}
204				if (button) {
205					button.removeClass("tvPressed");
206				}
207				tv.setComplete();
208				tv.updating = false;
209			},
210			timeout: function () {
211				if (button) {
212					button.removeClass("tvPressed");
213				}
214				tv.updating = false;
215				tv.setComplete();
216			}
217		});
218	} else {
219		if (button) {
220			button.removeClass("tvPressed");
221		}
222		tv.setComplete();
223	}
224	return false;
225};
226
227/**
228 * Class TreeView compact method
229 */
230TreeViewHandler.prototype.compact = function () {
231	var tv = this;
232	var b = $("#tvbCompact", tv.toolbox);
233	tv.setLoading();
234	if (tv.auto_box_width) {
235		var w = tv.boxWidth * (tv.zoom / 100) + "px";
236		var ew = tv.boxExpandedWidth * (tv.zoom / 100) + "px";
237		tv.treeview.find(".tv_box:not(boxExpanded)", tv.treeview).css("width", w);
238		tv.treeview.find(".boxExpanded", tv.treeview).css("width", ew);
239		tv.auto_box_width = false;
240		if (readCookie("compact")) {
241			createCookie("compact", false, tv.cookieDays);
242		}
243		b.removeClass("tvPressed");
244	} else {
245		tv.treeview.find(".tv_box").css("width", "auto");
246		tv.auto_box_width = true;
247		if (!readCookie("compact")) {
248			createCookie("compact", true, tv.cookieDays);
249		}
250		if (!tv.updating) {
251			tv.updateTree();
252		}
253		b.addClass("tvPressed");
254	}
255	tv.setComplete();
256	return false;
257};
258
259/**
260 * Class TreeView centerOnRoot method
261 */
262TreeViewHandler.prototype.centerOnRoot = function () {
263	this.loadingImage.css("display", "block");
264	var tv = this;
265	var tvc = this.container;
266	var tvc_width = tvc.innerWidth() / 2;
267	if (isNaN(tvc_width)) {
268		return false;
269	}
270	var tvc_height = tvc.innerHeight() / 2;
271	var root_person = $(".rootPerson", this.treeview);
272
273	if (!this.updating) {
274		tv.setComplete();
275	}
276	return false;
277};
278
279/**
280 * Class TreeView expandBox method
281 * param string @box the person box element
282 * param string @event the call event
283 * param string @pid the person id
284 *
285 * called ONLY for elements which have NOT the class tv_link to avoid unuseful requests to the server
286 */
287TreeViewHandler.prototype.expandBox = function (box, event) {
288	var t = $(event.target);
289	if (t.hasClass("tv_link")) {
290		return false;
291	}
292
293	var box = $(box, this.treeview);
294	var bc = box.parent(); // bc is Box Container
295	var pid = box.attr("abbr");
296	var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
297	var expanded;
298	var collapsed;
299
300	if (bc.hasClass("detailsLoaded")) {
301		collapsed = bc.find(".collapsedContent");
302		expanded = bc.find(".tv_box:not(.collapsedContent)");
303	} else {
304		// Cache the box content as an hidden person's box in the box's parent element
305		expanded = box;
306		collapsed = box.clone();
307		bc.append(collapsed.addClass("collapsedContent").css("display", "none"));
308		// we add a waiting image at the right side of the box
309		var loading_image = this.loadingImage.find("img").clone().addClass("tv_box_loading").css("display", "block");
310		box.prepend(loading_image);
311		tv.updating = true;
312		tv.setLoading();
313		// perform the Ajax request and load the result in the box
314		box.load(tv.ajaxDetails + "&pid=" + encodeURIComponent(pid), function () {
315			// If Lightbox module is active, we reinitialize it for the new links
316			if (typeof CB_Init === "function") {
317				CB_Init();
318			}
319			box.css("width", tv.boxExpandedWidth * (tv.zoom / 100) + "px");
320			loading_image.remove();
321			bc.addClass("detailsLoaded");
322			tv.setComplete();
323			tv.updating = false;
324		});
325	}
326	if (box.hasClass("boxExpanded")) {
327		expanded.css("display", "none");
328		collapsed.css("display", "block");
329		box.removeClass("boxExpanded");
330	} else {
331		expanded.css("display", "block");
332		collapsed.css("display", "none");
333		expanded.addClass("boxExpanded");
334	}
335	// we must ajust the draggable treeview size to its content size
336	this.getSize();
337	return false;
338};
339
340function createCookie(name, value, days) {
341	if (days) {
342		var date = new Date();
343		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
344		document.cookie = name + "=" + value + "; expires=" + date.toGMTString() + "; path=/";
345	} else {
346		document.cookie = name + "=" + value + "; path=/";
347	}
348}
349
350function readCookie(name) {
351	var name_equals = name + "=";
352	var ca = document.cookie.split(';');
353	for (var i = 0; i < ca.length; i++) {
354		var c = ca[i];
355		while (c.charAt(0) === ' ') {
356			c = c.substring(1, c.length);
357		}
358		if (c.indexOf(name_equals) === 0) {
359			return c.substring(name_equals.length, c.length);
360		}
361	}
362	return null;
363}
364