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