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