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